| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- "use client";
- // azure and openai, using same models. so using same LLMApi.
- import {
- ApiPath,
- DEFAULT_API_HOST,
- DEFAULT_MODELS,
- OpenaiPath,
- Azure,
- REQUEST_TIMEOUT_MS,
- ServiceProvider,
- } from "@/app/constant";
- import {
- ChatMessageTool,
- useAccessStore,
- useAppConfig,
- useChatStore,
- usePluginStore,
- } from "@/app/store";
- import { collectModelsWithDefaultModel } from "@/app/utils/model";
- import {
- preProcessImageContent,
- uploadImage,
- base64Image2Blob,
- stream,
- } from "@/app/utils/chat";
- import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
- import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
- import {
- ChatOptions,
- getHeaders,
- LLMApi,
- LLMModel,
- LLMUsage,
- MultimodalContent,
- SpeechOptions,
- } from "../api";
- import Locale from "../../locales";
- import { getClientConfig } from "@/app/config/client";
- import {
- getMessageTextContent,
- isVisionModel,
- isDalle3 as _isDalle3,
- } from "@/app/utils";
- export interface OpenAIListModelResponse {
- object: string;
- data: Array<{
- id: string;
- object: string;
- root: string;
- }>;
- }
- export interface RequestPayload {
- messages: {
- role: "system" | "user" | "assistant";
- content: string | MultimodalContent[];
- }[];
- stream?: boolean;
- model: string;
- temperature: number;
- presence_penalty: number;
- frequency_penalty: number;
- top_p: number;
- max_tokens?: number;
- }
- export interface DalleRequestPayload {
- model: string;
- prompt: string;
- response_format: "url" | "b64_json";
- n: number;
- size: DalleSize;
- quality: DalleQuality;
- style: DalleStyle;
- }
- export class ChatGPTApi implements LLMApi {
- private disableListModels = true;
- path(path: string): string {
- const accessStore = useAccessStore.getState();
- let baseUrl = "";
- const isAzure = path.includes("deployments");
- if (accessStore.useCustomConfig) {
- if (isAzure && !accessStore.isValidAzure()) {
- throw Error(
- "incomplete azure config, please check it in your settings page",
- );
- }
- baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
- }
- if (baseUrl.length === 0) {
- const isApp = !!getClientConfig()?.isApp;
- const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
- baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
- }
- if (baseUrl.endsWith("/")) {
- baseUrl = baseUrl.slice(0, baseUrl.length - 1);
- }
- if (
- !baseUrl.startsWith("http") &&
- !isAzure &&
- !baseUrl.startsWith(ApiPath.OpenAI)
- ) {
- baseUrl = "https://" + baseUrl;
- }
- console.log("[Proxy Endpoint] ", baseUrl, path);
- // try rebuild url, when using cloudflare ai gateway in client
- return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
- }
- async extractMessage(res: any) {
- if (res.error) {
- return "```\n" + JSON.stringify(res, null, 4) + "\n```";
- }
- // dalle3 model return url, using url create image message
- if (res.data) {
- let url = res.data?.at(0)?.url ?? "";
- const b64_json = res.data?.at(0)?.b64_json ?? "";
- if (!url && b64_json) {
- // uploadImage
- url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
- }
- return [
- {
- type: "image_url",
- image_url: {
- url,
- },
- },
- ];
- }
- return res.choices?.at(0)?.message?.content ?? res;
- }
- async speech(options: SpeechOptions): Promise<ArrayBuffer> {
- const requestPayload = {
- model: options.model,
- input: options.input,
- voice: options.voice,
- response_format: options.response_format,
- speed: options.speed,
- };
- console.log("[Request] openai speech payload: ", requestPayload);
- const controller = new AbortController();
- options.onController?.(controller);
- try {
- const speechPath = this.path(OpenaiPath.SpeechPath);
- const speechPayload = {
- method: "POST",
- body: JSON.stringify(requestPayload),
- signal: controller.signal,
- headers: getHeaders(),
- };
- // make a fetch request
- const requestTimeoutId = setTimeout(
- () => controller.abort(),
- REQUEST_TIMEOUT_MS,
- );
- const res = await fetch(speechPath, speechPayload);
- clearTimeout(requestTimeoutId);
- return await res.arrayBuffer();
- } catch (e) {
- console.log("[Request] failed to make a speech request", e);
- throw e;
- }
- }
- async chat(options: ChatOptions) {
- const modelConfig = {
- ...useAppConfig.getState().modelConfig,
- ...useChatStore.getState().currentSession().mask.modelConfig,
- ...{
- model: options.config.model,
- providerName: options.config.providerName,
- },
- };
- let requestPayload: RequestPayload | DalleRequestPayload;
- const isDalle3 = _isDalle3(options.config.model);
- const isO1 = options.config.model.startsWith("o1");
- if (isDalle3) {
- const prompt = getMessageTextContent(
- options.messages.slice(-1)?.pop() as any,
- );
- requestPayload = {
- model: options.config.model,
- prompt,
- // URLs are only valid for 60 minutes after the image has been generated.
- response_format: "b64_json", // using b64_json, and save image in CacheStorage
- n: 1,
- size: options.config?.size ?? "1024x1024",
- quality: options.config?.quality ?? "standard",
- style: options.config?.style ?? "vivid",
- };
- } else {
- const visionModel = isVisionModel(options.config.model);
- const messages: ChatOptions["messages"] = [];
- for (const v of options.messages) {
- const content = visionModel
- ? await preProcessImageContent(v.content)
- : getMessageTextContent(v);
- if (!(isO1 && v.role === "system"))
- messages.push({ role: v.role, content });
- }
- // O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
- requestPayload = {
- messages,
- stream: !isO1 ? options.config.stream : false,
- model: modelConfig.model,
- temperature: !isO1 ? modelConfig.temperature : 1,
- presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
- frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
- top_p: !isO1 ? modelConfig.top_p : 1,
- // max_tokens: Math.max(modelConfig.max_tokens, 1024),
- // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
- };
- // add max_tokens to vision model
- if (visionModel) {
- requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
- }
- }
- console.log("[Request] openai payload: ", requestPayload);
- const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
- const controller = new AbortController();
- options.onController?.(controller);
- try {
- let chatPath = "";
- if (modelConfig.providerName === ServiceProvider.Azure) {
- // find model, and get displayName as deployName
- const { models: configModels, customModels: configCustomModels } =
- useAppConfig.getState();
- const {
- defaultModel,
- customModels: accessCustomModels,
- useCustomConfig,
- } = useAccessStore.getState();
- const models = collectModelsWithDefaultModel(
- configModels,
- [configCustomModels, accessCustomModels].join(","),
- defaultModel,
- );
- const model = models.find(
- (model) =>
- model.name === modelConfig.model &&
- model?.provider?.providerName === ServiceProvider.Azure,
- );
- chatPath = this.path(
- (isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
- (model?.displayName ?? model?.name) as string,
- useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
- ),
- );
- } else {
- chatPath = this.path(
- isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
- );
- }
- if (shouldStream) {
- const [tools, funcs] = usePluginStore
- .getState()
- .getAsTools(
- useChatStore.getState().currentSession().mask?.plugin || [],
- );
- // console.log("getAsTools", tools, funcs);
- stream(
- chatPath,
- requestPayload,
- getHeaders(),
- tools as any,
- funcs,
- controller,
- // parseSSE
- (text: string, runTools: ChatMessageTool[]) => {
- // console.log("parseSSE", text, runTools);
- const json = JSON.parse(text);
- const choices = json.choices as Array<{
- delta: {
- content: string;
- tool_calls: ChatMessageTool[];
- };
- }>;
- const tool_calls = choices[0]?.delta?.tool_calls;
- if (tool_calls?.length > 0) {
- const index = tool_calls[0]?.index;
- const id = tool_calls[0]?.id;
- const args = tool_calls[0]?.function?.arguments;
- if (id) {
- runTools.push({
- id,
- type: tool_calls[0]?.type,
- function: {
- name: tool_calls[0]?.function?.name as string,
- arguments: args,
- },
- });
- } else {
- // @ts-ignore
- runTools[index]["function"]["arguments"] += args;
- }
- }
- return choices[0]?.delta?.content;
- },
- // processToolMessage, include tool_calls message and tool call results
- (
- requestPayload: RequestPayload,
- toolCallMessage: any,
- toolCallResult: any[],
- ) => {
- // @ts-ignore
- requestPayload?.messages?.splice(
- // @ts-ignore
- requestPayload?.messages?.length,
- 0,
- toolCallMessage,
- ...toolCallResult,
- );
- },
- options,
- );
- } else {
- const chatPayload = {
- method: "POST",
- body: JSON.stringify(requestPayload),
- signal: controller.signal,
- headers: getHeaders(),
- };
- // make a fetch request
- const requestTimeoutId = setTimeout(
- () => controller.abort(),
- isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
- );
- const res = await fetch(chatPath, chatPayload);
- clearTimeout(requestTimeoutId);
- const resJson = await res.json();
- const message = await this.extractMessage(resJson);
- options.onFinish(message);
- }
- } catch (e) {
- console.log("[Request] failed to make a chat request", e);
- options.onError?.(e as Error);
- }
- }
- async usage() {
- const formatDate = (d: Date) =>
- `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
- .getDate()
- .toString()
- .padStart(2, "0")}`;
- const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
- const now = new Date();
- const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
- const startDate = formatDate(startOfMonth);
- const endDate = formatDate(new Date(Date.now() + ONE_DAY));
- const [used, subs] = await Promise.all([
- fetch(
- this.path(
- `${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
- ),
- {
- method: "GET",
- headers: getHeaders(),
- },
- ),
- fetch(this.path(OpenaiPath.SubsPath), {
- method: "GET",
- headers: getHeaders(),
- }),
- ]);
- if (used.status === 401) {
- throw new Error(Locale.Error.Unauthorized);
- }
- if (!used.ok || !subs.ok) {
- throw new Error("Failed to query usage from openai");
- }
- const response = (await used.json()) as {
- total_usage?: number;
- error?: {
- type: string;
- message: string;
- };
- };
- const total = (await subs.json()) as {
- hard_limit_usd?: number;
- };
- if (response.error && response.error.type) {
- throw Error(response.error.message);
- }
- if (response.total_usage) {
- response.total_usage = Math.round(response.total_usage) / 100;
- }
- if (total.hard_limit_usd) {
- total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
- }
- return {
- used: response.total_usage,
- total: total.hard_limit_usd,
- } as LLMUsage;
- }
- async models(): Promise<LLMModel[]> {
- if (this.disableListModels) {
- return DEFAULT_MODELS.slice();
- }
- const res = await fetch(this.path(OpenaiPath.ListModelPath), {
- method: "GET",
- headers: {
- ...getHeaders(),
- },
- });
- const resJson = (await res.json()) as OpenAIListModelResponse;
- const chatModels = resJson.data?.filter(
- (m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
- );
- console.log("[Models]", chatModels);
- if (!chatModels) {
- return [];
- }
- //由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场
- let seq = 1000; //同 Constant.ts 中的排序保持一致
- return chatModels.map((m) => ({
- name: m.id,
- available: true,
- sorted: seq++,
- provider: {
- id: "openai",
- providerName: "OpenAI",
- providerType: "openai",
- sorted: 1,
- },
- }));
- }
- }
- export { OpenaiPath };
|