Skip to content

Commit

Permalink
feat: add Azure OpenAI service support
Browse files Browse the repository at this point in the history
  • Loading branch information
KeJunMao committed Mar 17, 2023
1 parent 276b138 commit 810ac41
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 46 deletions.
50 changes: 48 additions & 2 deletions components/app/AppGPTSettingForms.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<script lang="ts" setup>
const { storageOptions: options } = useChatGPT();
options.value = {
...defaultChatGPTOptions,
...options.value,
};
const models = [
"gpt-4",
"gpt-4-0314",
Expand All @@ -8,6 +13,31 @@ const models = [
"gpt-3.5-turbo",
"gpt-3.5-turbo-0301",
];
const defaultAzureApiURLPath = "/openai/deployments";
const defaultAzureapiURL = "https://please-replace-this.openai.azure.com";
watch(
() => options.value.provider,
() => {
if (options.value.provider === "Azure") {
if (options.value.apiUrlPath === defaultChatGPTOptions.apiUrlPath) {
options.value.apiUrlPath = defaultAzureApiURLPath;
}
if (options.value.apiBaseUrl === defaultChatGPTOptions.apiBaseUrl) {
options.value.apiBaseUrl = defaultAzureapiURL;
}
options.value.model = "gpt-3.5-turbo";
} else {
if (options.value.apiUrlPath === defaultAzureApiURLPath) {
options.value.apiUrlPath = defaultChatGPTOptions.apiUrlPath;
}
if (options.value.apiBaseUrl === defaultAzureapiURL) {
options.value.apiBaseUrl = defaultChatGPTOptions.apiBaseUrl;
}
}
}
);
</script>

<template>
Expand All @@ -18,14 +48,30 @@ const models = [
placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
></el-input>
</el-form-item>
<el-form-item :label="$t('app.gpt-setting-forms.api-base-url')">
<el-form-item :label="$t('app.gpt-setting-forms.default-provider')">
<el-select v-model="options.provider" w-full>
<el-option value="OpenAI"></el-option>
<el-option value="Azure"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('app.gpt-setting-forms.api-url')">
<el-input
v-model="options.apiBaseUrl"
placeholder="https://api.openai.com"
></el-input>
</el-form-item>
<el-form-item :label="$t('app.gpt-setting-forms.api-url-path')">
<el-input
v-model="options.apiUrlPath"
placeholder="/v1/chat/completions"
></el-input>
</el-form-item>
<el-form-item :label="$t('app.gpt-setting-forms.model')">
<el-select w-full v-model="options.model">
<el-select
w-full
v-model="options.model"
:disabled="options.provider === 'Azure'"
>
<el-option v-for="item in models" :value="item"></el-option>
</el-select>
</el-form-item>
Expand Down
4 changes: 3 additions & 1 deletion composables/useAi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export const useAi = (_tool: MaybeRef<ToolItem>) => {
options.value
);
} catch (error: any) {
result.value = error?.message ?? String(error);
result.value = error.data
? JSON.stringify(error.data, null, 2)
: null ?? error?.message ?? error;
}
loading.value = false;
};
Expand Down
114 changes: 75 additions & 39 deletions composables/useChatGPT.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { STORAGE_KEY_GPT_SETTINGS } from "~/constants";
import { ChatGPTMessages, ChatGPTOptions } from "~~/types";
import { OpenAIMessages, ChatGPTOptions } from "~~/types";
import { convertOpenAIMessagesToAzurePrompt } from "~~/utils/azure";
import { createParser } from "eventsource-parser";
import { streamAsyncIterator } from "~~/utils";

export const defaultChatGPTOptions: ChatGPTOptions = {
apiKey: "",
apiBaseUrl: "https://api.openai.com",
apiUrlPath: "/v1/chat/completions",
provider: "OpenAI",
model: "gpt-3.5-turbo",
temperature: 1,
top_p: 1,
Expand All @@ -14,56 +19,87 @@ export const defaultChatGPTOptions: ChatGPTOptions = {
frequency_penalty: 0,
};

function createFetchGPTResponse(
options: ChatGPTOptions,
messages: OpenAIMessages
) {
const { apiKey, apiBaseUrl, apiUrlPath, provider, ...opts } = options;

const body: Record<string, any> = {
...opts,
};
const headers: Record<string, any> = {
"Content-Type": "application/json",
};
let url = apiUrlPath;
switch (provider) {
case "OpenAI":
headers["Authorization"] = `Bearer ${apiKey}`;
body["messages"] = messages;
break;
case "Azure":
headers["api-key"] = `${apiKey}`;
url = `${apiUrlPath}/${options.model}/completions?api-version=2022-12-01`;
Object.assign(body, convertOpenAIMessagesToAzurePrompt(messages));
break;
}

return $fetch.raw(url, {
baseURL: apiBaseUrl,
headers,
body,
method: "post",
responseType: "stream",
});
}

export const useChatGPT = createSharedComposable(() => {
const storageOptions = useLocalStorage(
STORAGE_KEY_GPT_SETTINGS,
defaultChatGPTOptions
);
const sendMessage = async (
messages: ChatGPTMessages,
onProgress?: (message: string, data: Record<string, any>) => void,
messages: OpenAIMessages,
onProgress: (data: string) => void = () => {},
options: Partial<ChatGPTOptions> = {}
) => {
const { apiKey, apiBaseUrl, ...opts } = {
options = {
...storageOptions.value,
...options,
};
// @ts-ignroe
const response: ReadableStream = await $fetch("/v1/chat/completions", {
baseURL: apiBaseUrl,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
method: "post",
body: {
...opts,
messages,
},
responseType: "stream",
});

const reader = response.getReader();
const decoder = new TextDecoder();
let isDone = false;
while (!isDone) {
const { value, done } = await reader.read();
isDone = done;
const text = decoder.decode(value);
const data = text
.split("data:")
.filter((v) => !!v.trim() && v.trim() !== "[DONE]")
.map((v) => JSON.parse(v));

const message = data
.map((v) => {
const msg = v.choices[0];
return msg.message ? msg.message.content : msg.delta.content;
})
.join("");
if (onProgress) {
onProgress(message, data);
// @ts-expect-error
const resp = await createFetchGPTResponse(options, messages);
const parser = createParser((event) => {
if (event.type === "event") {
let data: Record<string, any>;
try {
data = JSON.parse(event.data);
} catch {
console.log("Failed to parse event data", event.data);
return;
}
const { choices } = data;
if (!choices || choices.length === 0) {
throw new Error(`No choices found in response`);
}
let message = "";
switch (options.provider) {
case "OpenAI":
const { content = "" } = choices[0].delta;
message = content;
break;
case "Azure":
message = choices[0].text;
break;
}
onProgress(message);
}
});
for await (const chunk of streamAsyncIterator[Symbol.asyncIterator](
resp.body
)) {
const str = new TextDecoder().decode(chunk);
parser.feed(str);
}
};
return { sendMessage, storageOptions };
Expand Down
5 changes: 4 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,8 @@
"workshop.header.desc": "Discover our list of tool to supercharge your AIAnything home page. Created by the community.",
"workshop.search.placeholder": "Search for tools from workshop",
"app.name": "AI Anything",
"app.desc": "Anyone can create GPT tools, AI Anything makes it possible for everyone to create ChatGPT widgets quickly"
"app.desc": "Anyone can create GPT tools, AI Anything makes it possible for everyone to create ChatGPT widgets quickly",
"app.gpt-setting-forms.api-url": "API URL",
"app.gpt-setting-forms.api-url-path": "API URL Path",
"app.gpt-setting-forms.default-provider": "Default Provider"
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@unocss/reset": "^0.50.4",
"@vueuse/core": "^9.13.0",
"element-plus": "^2.3.0",
"eventsource-parser": "^0.1.0",
"marked": "^4.2.12",
"next-auth": "4.20.1",
"std-env": "^3.3.2",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion types/chatgpt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export type ChatGPTProvider = "OpenAI" | "Azure";

export interface ChatGPTOptions {
apiKey: string;
apiBaseUrl: string;
apiUrlPath: string;
provider: ChatGPTProvider;
model: "gpt-3.5-turbo" | "gpt-3.5-turbo-0301";
temperature?: number;
top_p?: number;
Expand All @@ -16,4 +20,4 @@ export interface ChatGPTMessage {
content: string;
}

export type ChatGPTMessages = ChatGPTMessage[];
export type OpenAIMessages = ChatGPTMessage[];
11 changes: 11 additions & 0 deletions utils/azure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OpenAIMessages } from "~~/types";

export function convertOpenAIMessagesToAzurePrompt(messages: OpenAIMessages) {
const msgs = messages
.map((v) => `<|im_start|>${v.role}\n${v.content}\n<|im_end|>`)
.join("\n");
return {
prompt: `${msgs}\n<|im_start|>assistant\n`,
stop: ["<|im_end|>"],
};
}
23 changes: 23 additions & 0 deletions utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export async function* streamAsyncIterable(
stream: ReadableStream<Uint8Array> | null
) {
if (!stream) {
return;
}
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
yield value;
}
} finally {
reader.releaseLock();
}
}

export const streamAsyncIterator = {
[Symbol.asyncIterator]: streamAsyncIterable,
};
4 changes: 2 additions & 2 deletions utils/openai.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChatGPTMessages, ToolItemRole } from "~~/types";
import { OpenAIMessages, ToolItemRole } from "~~/types";

export const parseTemplate = (
data: Record<string, any>,
Expand All @@ -18,5 +18,5 @@ export const parseRoles = (
role: v.type,
content: parseTemplate(data, v.template) ?? "",
};
}) as ChatGPTMessages;
}) as OpenAIMessages;
};

0 comments on commit 810ac41

Please sign in to comment.