Merge pull request #2382 from glconti/main

feat(openai): add Azure OpenAI support with configuration options
This commit is contained in:
Amruth Pillai
2025-10-01 10:56:07 +02:00
committed by GitHub
4 changed files with 141 additions and 14 deletions

View File

@ -1,2 +1,3 @@
export const DEFAULT_MODEL = "gpt-3.5-turbo"; export const DEFAULT_MODEL = "gpt-3.5-turbo";
export const DEFAULT_MAX_TOKENS = 1024; export const DEFAULT_MAX_TOKENS = 1024;
export const DEFAULT_AZURE_API_VERSION = "2024-10-21";

View File

@ -11,11 +11,12 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
Input, Input,
Checkbox,
} from "@reactive-resume/ui"; } from "@reactive-resume/ui";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { DEFAULT_MAX_TOKENS, DEFAULT_MODEL } from "@/client/constants/llm"; import { DEFAULT_MAX_TOKENS, DEFAULT_MODEL, DEFAULT_AZURE_API_VERSION } from "@/client/constants/llm";
import { useOpenAiStore } from "@/client/stores/openai"; import { useOpenAiStore } from "@/client/stores/openai";
const formSchema = z.object({ const formSchema = z.object({
@ -32,13 +33,21 @@ const formSchema = z.object({
.default(""), .default(""),
model: z.string().default(DEFAULT_MODEL), model: z.string().default(DEFAULT_MODEL),
maxTokens: z.number().default(DEFAULT_MAX_TOKENS), maxTokens: z.number().default(DEFAULT_MAX_TOKENS),
isAzure: z.boolean().default(false),
azureApiVersion: z.string().default(DEFAULT_AZURE_API_VERSION),
}); });
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
export const OpenAISettings = () => { export const OpenAISettings = () => {
const { apiKey, setApiKey, baseURL, setBaseURL, model, setModel, maxTokens, setMaxTokens } = const {
useOpenAiStore(); apiKey, setApiKey,
baseURL, setBaseURL,
model, setModel,
maxTokens, setMaxTokens,
isAzure, setIsAzure,
azureApiVersion, setAzureApiVersion
} = useOpenAiStore();
const isEnabled = !!apiKey; const isEnabled = !!apiKey;
@ -49,11 +58,14 @@ export const OpenAISettings = () => {
baseURL: baseURL ?? "", baseURL: baseURL ?? "",
model: model ?? DEFAULT_MODEL, model: model ?? DEFAULT_MODEL,
maxTokens: maxTokens ?? DEFAULT_MAX_TOKENS, maxTokens: maxTokens ?? DEFAULT_MAX_TOKENS,
isAzure: isAzure ?? false,
azureApiVersion: azureApiVersion ?? DEFAULT_AZURE_API_VERSION,
}, },
}); });
const onSubmit = ({ apiKey, baseURL, model, maxTokens }: FormValues) => { const onSubmit = ({ apiKey, baseURL, model, maxTokens, isAzure, azureApiVersion }: FormValues) => {
setApiKey(apiKey); setApiKey(apiKey);
setIsAzure(isAzure);
if (baseURL) { if (baseURL) {
setBaseURL(baseURL); setBaseURL(baseURL);
} }
@ -63,6 +75,9 @@ export const OpenAISettings = () => {
if (maxTokens) { if (maxTokens) {
setMaxTokens(maxTokens); setMaxTokens(maxTokens);
} }
if (azureApiVersion) {
setAzureApiVersion(azureApiVersion);
}
}; };
const onRemove = () => { const onRemove = () => {
@ -70,15 +85,24 @@ export const OpenAISettings = () => {
setBaseURL(null); setBaseURL(null);
setModel(DEFAULT_MODEL); setModel(DEFAULT_MODEL);
setMaxTokens(DEFAULT_MAX_TOKENS); setMaxTokens(DEFAULT_MAX_TOKENS);
form.reset({ apiKey: "", baseURL: "", model: DEFAULT_MODEL, maxTokens: DEFAULT_MAX_TOKENS }); setIsAzure(false);
setAzureApiVersion(DEFAULT_AZURE_API_VERSION);
form.reset({
apiKey: "",
baseURL: "",
model: DEFAULT_MODEL,
maxTokens: DEFAULT_MAX_TOKENS,
isAzure: false,
azureApiVersion: DEFAULT_AZURE_API_VERSION
});
}; };
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`OpenAI/Ollama Integration`}</h3> <h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`OpenAI/Azure OpenAI/Ollama Integration`}</h3>
<p className="leading-relaxed opacity-75"> <p className="leading-relaxed opacity-75">
{t`You can make use of the OpenAI API to help you generate content, or improve your writing while composing your resume.`} {t`You can make use of the OpenAI API, Azure OpenAI, or Ollama to help you generate content, or improve your writing while composing your resume.`}
</p> </p>
</div> </div>
@ -99,11 +123,20 @@ export const OpenAISettings = () => {
</Trans> </Trans>
</p> </p>
<p>
<Trans>
You can also integrate with Azure OpenAI by enabling the "Use Azure OpenAI" checkbox
and setting the Resource URL to your Azure OpenAI resource (e.g.,
<code>https://your-resource.openai.azure.com</code>). Set the deployment name in the Model field
and specify the appropriate API version for your Azure deployment.
</Trans>
</p>
<p> <p>
<Trans> <Trans>
You can also integrate with Ollama simply by setting the API key to You can also integrate with Ollama simply by setting the API key to
`sk-1234567890abcdef` and the Base URL to your Ollama URL, i.e. <code>sk-1234567890abcdef</code> and the Base URL to your Ollama URL, i.e.
`http://localhost:11434/v1`. You can also pick and choose models and set the max tokens <code>http://localhost:11434/v1</code>. You can also pick and choose models and set the max tokens
as per your preference. as per your preference.
</Trans> </Trans>
</p> </p>
@ -129,9 +162,22 @@ export const OpenAISettings = () => {
control={form.control} control={form.control}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t`Base URL`}</FormLabel> <FormLabel>
{form.watch("isAzure")
? t`Azure OpenAI Resource URL`
: t`Base URL`
}
</FormLabel>
<FormControl> <FormControl>
<Input type="text" placeholder="http://localhost:11434/v1" {...field} /> <Input
type="text"
placeholder={
form.watch("isAzure")
? "https://your-resource.openai.azure.com"
: "http://localhost:11434/v1"
}
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -142,7 +188,12 @@ export const OpenAISettings = () => {
control={form.control} control={form.control}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t`Model`}</FormLabel> <FormLabel>
{form.watch("isAzure")
? t`Deployment Name`
: t`Model`
}
</FormLabel>
<FormControl> <FormControl>
<Input type="text" placeholder={DEFAULT_MODEL} {...field} /> <Input type="text" placeholder={DEFAULT_MODEL} {...field} />
</FormControl> </FormControl>
@ -170,6 +221,44 @@ export const OpenAISettings = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
name="isAzure"
control={form.control}
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={!!field.value}
onCheckedChange={(value) => {
field.onChange(Boolean(value));
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>{t`Use Azure OpenAI`}</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="azureApiVersion"
control={form.control}
render={({ field }) => (
<FormItem className={form.watch("isAzure") ? "" : "opacity-50"}>
<FormLabel>{t`Azure API Version`}</FormLabel>
<FormControl>
<Input
type="text"
placeholder={DEFAULT_AZURE_API_VERSION}
disabled={!form.watch("isAzure")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center space-x-2 self-end sm:col-start-2"> <div className="flex items-center space-x-2 self-end sm:col-start-2">
<Button type="submit" disabled={!form.formState.isValid}> <Button type="submit" disabled={!form.formState.isValid}>
{isEnabled && <FloppyDiskIcon className="mr-2" />} {isEnabled && <FloppyDiskIcon className="mr-2" />}

View File

@ -4,7 +4,7 @@ import { OpenAI } from "openai";
import { useOpenAiStore } from "@/client/stores/openai"; import { useOpenAiStore } from "@/client/stores/openai";
export const openai = () => { export const openai = () => {
const { apiKey, baseURL } = useOpenAiStore.getState(); const { apiKey, baseURL, isAzure, azureApiVersion, model } = useOpenAiStore.getState();
if (!apiKey) { if (!apiKey) {
throw new Error( throw new Error(
@ -12,6 +12,31 @@ export const openai = () => {
); );
} }
if (isAzure) {
if (!baseURL) {
throw new Error(t`Azure OpenAI Base URL is required when using Azure OpenAI.`);
}
if (!model) {
throw new Error(t`Azure OpenAI deployment name (model) is required when using Azure OpenAI.`);
}
if (!azureApiVersion) {
throw new Error(t`Azure OpenAI API version is required when using Azure OpenAI.`);
}
// Construct Azure OpenAI URL: https://your-resource.openai.azure.com/openai/deployments/your-deployment
const azureBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
const constructedURL = `${azureBaseURL}/openai/deployments/${model}`;
return new OpenAI({
apiKey,
baseURL: constructedURL,
defaultQuery: { "api-version": azureApiVersion ?? undefined },
dangerouslyAllowBrowser: true,
});
}
if (baseURL) { if (baseURL) {
return new OpenAI({ return new OpenAI({
apiKey, apiKey,

View File

@ -1,7 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { DEFAULT_MAX_TOKENS, DEFAULT_MODEL } from "../constants/llm"; import { DEFAULT_MAX_TOKENS, DEFAULT_MODEL, DEFAULT_AZURE_API_VERSION } from "../constants/llm";
type OpenAIStore = { type OpenAIStore = {
baseURL: string | null; baseURL: string | null;
@ -12,6 +12,10 @@ type OpenAIStore = {
setModel: (model: string | null) => void; setModel: (model: string | null) => void;
maxTokens: number | null; maxTokens: number | null;
setMaxTokens: (maxTokens: number | null) => void; setMaxTokens: (maxTokens: number | null) => void;
isAzure: boolean;
setIsAzure: (isAzure: boolean) => void;
azureApiVersion: string | null;
setAzureApiVersion: (apiVersion: string | null) => void;
}; };
export const useOpenAiStore = create<OpenAIStore>()( export const useOpenAiStore = create<OpenAIStore>()(
@ -33,6 +37,14 @@ export const useOpenAiStore = create<OpenAIStore>()(
setMaxTokens: (maxTokens: number | null) => { setMaxTokens: (maxTokens: number | null) => {
set({ maxTokens }); set({ maxTokens });
}, },
isAzure: false,
setIsAzure: (isAzure: boolean) => {
set({ isAzure });
},
azureApiVersion: DEFAULT_AZURE_API_VERSION,
setAzureApiVersion: (azureApiVersion: string | null) => {
set({ azureApiVersion });
},
}), }),
{ name: "openai" }, { name: "openai" },
), ),