diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts index 78106efa76c..20f8d52e062 100644 --- a/app/api/anthropic/[...path]/route.ts +++ b/app/api/anthropic/[...path]/route.ts @@ -11,6 +11,7 @@ import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "../../auth"; import { isModelAvailableInServer } from "@/app/utils/model"; +import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); @@ -114,7 +115,8 @@ async function request(req: NextRequest) { 10 * 60 * 1000, ); - const fetchUrl = `${baseUrl}${path}`; + // try rebuild url, when using cloudflare ai gateway in server + const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`); const fetchOptions: RequestInit = { headers: { @@ -164,17 +166,17 @@ async function request(req: NextRequest) { console.error(`[Anthropic] filter`, e); } } - console.log("[Anthropic request]", fetchOptions.headers, req.method); + // console.log("[Anthropic request]", fetchOptions.headers, req.method); try { const res = await fetch(fetchUrl, fetchOptions); - console.log( - "[Anthropic response]", - res.status, - " ", - res.headers, - res.url, - ); + // console.log( + // "[Anthropic response]", + // res.status, + // " ", + // res.headers, + // res.url, + // ); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate"); diff --git a/app/api/common.ts b/app/api/common.ts index 1ffac7fce15..24453dd9635 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -7,6 +7,7 @@ import { ServiceProvider, } from "../constant"; import { isModelAvailableInServer } from "../utils/model"; +import { cloudflareAIGatewayUrl } from "../utils/cloudflare"; const serverConfig = getServerSideConfig(); @@ -37,7 +38,7 @@ export async function requestOpenai(req: NextRequest) { ); let baseUrl = - serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL; + (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; if (!baseUrl.startsWith("http")) { baseUrl = `https://${baseUrl}`; @@ -95,7 +96,8 @@ export async function requestOpenai(req: NextRequest) { } } - const fetchUrl = `${baseUrl}/${path}`; + const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`); + console.log("fetchUrl", fetchUrl); const fetchOptions: RequestInit = { headers: { "Content-Type": "application/json", diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 18c3decac8e..bf8faf83763 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -1,5 +1,5 @@ import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; -import { ChatOptions, getHeaders, LLMApi, MultimodalContent, } from "../api"; +import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { getClientConfig } from "@/app/config/client"; import { DEFAULT_API_HOST } from "@/app/constant"; @@ -12,6 +12,7 @@ import { import Locale from "../../locales"; import { prettyObject } from "@/app/utils/format"; import { getMessageTextContent, isVisionModel } from "@/app/utils"; +import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; export type MultiBlockContent = { type: "image" | "text"; @@ -190,7 +191,7 @@ export class ClaudeApi implements LLMApi { body: JSON.stringify(requestBody), signal: controller.signal, headers: { - ...getHeaders(), // get common headers + ...getHeaders(), // get common headers "anthropic-version": accessStore.anthropicApiVersion, // do not send `anthropicApiKey` in browser!!! // Authorization: getAuthKey(accessStore.anthropicApiKey), @@ -375,7 +376,8 @@ export class ClaudeApi implements LLMApi { baseUrl = trimEnd(baseUrl, "/"); - return `${baseUrl}/${path}`; + // try rebuild url, when using cloudflare ai gateway in client + return cloudflareAIGatewayUrl(`${baseUrl}/${path}`); } } diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 828b28a0d4d..6054c7a476e 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -122,16 +122,13 @@ export class GeminiProApi implements LLMApi { const controller = new AbortController(); options.onController?.(controller); try { - // let baseUrl = accessStore.googleUrl; - - if (!baseUrl) { - baseUrl = isApp - ? DEFAULT_API_HOST + - "/api/proxy/google/" + - Google.ChatPath(modelConfig.model) - : this.path(Google.ChatPath(modelConfig.model)); + if (!baseUrl && isApp) { + baseUrl = DEFAULT_API_HOST + "/api/proxy/google/"; } - + baseUrl = `${baseUrl}/${Google.ChatPath(modelConfig.model)}`.replaceAll( + "//", + "/", + ); if (isApp) { baseUrl += `?key=${accessStore.googleApiKey}`; } diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index bba359429fc..98851c224c1 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -11,6 +11,7 @@ import { } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { collectModelsWithDefaultModel } from "@/app/utils/model"; +import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { ChatOptions, @@ -94,7 +95,8 @@ export class ChatGPTApi implements LLMApi { console.log("[Proxy Endpoint] ", baseUrl, path); - return [baseUrl, path].join("/"); + // try rebuild url, when using cloudflare ai gateway in client + return cloudflareAIGatewayUrl([baseUrl, path].join("/")); } extractMessage(res: any) { diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 3c5ab5e0617..00306b393af 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1195,8 +1195,7 @@ export function Settings() { { - return `${overview.chat} 次對話,${overview.message} 條訊息,${overview.prompt} 條提示詞,${overview.mask} 個角色範本`; + return `${overview.chat} 次對話,${overview.message} 則訊息,${overview.prompt} 條提示詞,${overview.mask} 個角色範本`; }, ImportFailed: "匯入失敗", }, @@ -239,13 +239,13 @@ const tw = { Title: "停用提示詞自動補齊", SubTitle: "在輸入框開頭輸入 / 即可觸發自動補齊", }, - List: "自定義提示詞列表", + List: "自訂提示詞列表", ListCount: (builtin: number, custom: number) => - `內建 ${builtin} 條,使用者定義 ${custom} 條`, + `內建 ${builtin} 條,使用者自訂 ${custom} 條`, Edit: "編輯", Modal: { Title: "提示詞列表", - Add: "新增一條", + Add: "新增一則", Search: "搜尋提示詞", }, EditModal: { @@ -278,40 +278,40 @@ const tw = { Placeholder: "請輸入存取密碼", }, CustomEndpoint: { - Title: "自定義介面 (Endpoint)", - SubTitle: "是否使用自定義 Azure 或 OpenAI 服務", + Title: "自訂 API 端點 (Endpoint)", + SubTitle: "是否使用自訂 Azure 或 OpenAI 服務", }, Provider: { - Title: "模型服務商", - SubTitle: "切換不同的服務商", + Title: "模型供應商", + SubTitle: "切換不同的服務供應商", }, OpenAI: { ApiKey: { Title: "API Key", - SubTitle: "使用自定義 OpenAI Key 繞過密碼存取限制", + SubTitle: "使用自訂 OpenAI Key 繞過密碼存取限制", Placeholder: "OpenAI API Key", }, Endpoint: { - Title: "介面(Endpoint) 地址", - SubTitle: "除預設地址外,必須包含 http(s)://", + Title: "API 端點 (Endpoint) 位址", + SubTitle: "除預設位址外,必須包含 http(s)://", }, }, Azure: { ApiKey: { - Title: "介面金鑰", - SubTitle: "使用自定義 Azure Key 繞過密碼存取限制", + Title: "API 金鑰", + SubTitle: "使用自訂 Azure Key 繞過密碼存取限制", Placeholder: "Azure API Key", }, Endpoint: { - Title: "介面(Endpoint) 地址", - SubTitle: "樣例:", + Title: "API 端點 (Endpoint) 位址", + SubTitle: "範例:", }, ApiVerion: { - Title: "介面版本 (azure api version)", - SubTitle: "選擇指定的部分版本", + Title: "API 版本 (azure api version)", + SubTitle: "指定一個特定的 API 版本", }, }, Anthropic: { @@ -322,13 +322,13 @@ const tw = { }, Endpoint: { - Title: "終端地址", + Title: "端點位址", SubTitle: "範例:", }, ApiVerion: { Title: "API 版本 (claude api version)", - SubTitle: "選擇一個特定的 API 版本輸入", + SubTitle: "指定一個特定的 API 版本", }, }, Google: { @@ -339,7 +339,7 @@ const tw = { }, Endpoint: { - Title: "終端地址", + Title: "端點位址", SubTitle: "範例:", }, @@ -349,8 +349,8 @@ const tw = { }, }, CustomModel: { - Title: "自定義模型名", - SubTitle: "增加自定義模型可選項,使用英文逗號隔開", + Title: "自訂模型名稱", + SubTitle: "增加自訂模型可選擇項目,使用英文逗號隔開", }, }, @@ -400,7 +400,7 @@ const tw = { Context: { Toast: (x: any) => `已設定 ${x} 條前置上下文`, Edit: "前置上下文和歷史記憶", - Add: "新增一條", + Add: "新增一則", Clear: "上下文已清除", Revert: "恢復上下文", }, @@ -425,16 +425,16 @@ const tw = { EditModal: { Title: (readonly: boolean) => `編輯預設角色範本 ${readonly ? "(唯讀)" : ""}`, - Download: "下載預設", - Clone: "複製預設", + Download: "下載預設值", + Clone: "以此預設值建立副本", }, Config: { Avatar: "角色頭像", Name: "角色名稱", Sync: { - Title: "使用全域性設定", - SubTitle: "目前對話是否使用全域性模型設定", - Confirm: "目前對話的自定義設定將會被自動覆蓋,確認啟用全域性設定?", + Title: "使用全域設定", + SubTitle: "目前對話是否使用全域模型設定", + Confirm: "目前對話的自訂設定將會被自動覆蓋,確認啟用全域設定?", }, HideContext: { Title: "隱藏預設對話", @@ -450,15 +450,15 @@ const tw = { NewChat: { Return: "返回", Skip: "跳過", - NotShow: "不再呈現", + NotShow: "不再顯示", ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。", Title: "挑選一個角色範本", SubTitle: "現在開始,與角色範本背後的靈魂思維碰撞", More: "搜尋更多", }, URLCommand: { - Code: "檢測到連結中已經包含存取密碼,是否自動填入?", - Settings: "檢測到連結中包含了預設設定,是否自動填入?", + Code: "偵測到連結中已經包含存取密碼,是否自動填入?", + Settings: "偵測到連結中包含了預設設定,是否自動填入?", }, UI: { Confirm: "確認", diff --git a/app/store/access.ts b/app/store/access.ts index c0fea9fea2a..26359e55c91 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -12,15 +12,33 @@ import { DEFAULT_CONFIG } from "./config"; let fetchState = 0; // 0 not fetch, 1 fetching, 2 done -const DEFAULT_OPENAI_URL = - getClientConfig()?.buildMode === "export" - ? DEFAULT_API_HOST + "/api/proxy/openai" - : ApiPath.OpenAI; +const isApp = getClientConfig()?.buildMode === "export"; -const DEFAULT_AZURE_URL = - getClientConfig()?.buildMode === "export" - ? DEFAULT_API_HOST + "/api/proxy/azure/{resource_name}" - : ApiPath.Azure; +const DEFAULT_OPENAI_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/openai" + : ApiPath.OpenAI; + +const DEFAULT_GOOGLE_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/google" + : ApiPath.Google; + +const DEFAULT_ANTHROPIC_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/anthropic" + : ApiPath.Anthropic; + +const DEFAULT_BAIDU_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/baidu" + : ApiPath.Baidu; + +const DEFAULT_BYTEDANCE_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/bytedance" + : ApiPath.ByteDance; + +const DEFAULT_ALIBABA_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/alibaba" + : ApiPath.Alibaba; + +console.log("DEFAULT_ANTHROPIC_URL", DEFAULT_ANTHROPIC_URL); const DEFAULT_ACCESS_STATE = { accessCode: "", @@ -33,31 +51,31 @@ const DEFAULT_ACCESS_STATE = { openaiApiKey: "", // azure - azureUrl: DEFAULT_AZURE_URL, + azureUrl: "", azureApiKey: "", azureApiVersion: "2023-08-01-preview", // google ai studio - googleUrl: "", + googleUrl: DEFAULT_GOOGLE_URL, googleApiKey: "", googleApiVersion: "v1", // anthropic + anthropicUrl: DEFAULT_ANTHROPIC_URL, anthropicApiKey: "", anthropicApiVersion: "2023-06-01", - anthropicUrl: "", // baidu - baiduUrl: "", + baiduUrl: DEFAULT_BAIDU_URL, baiduApiKey: "", baiduSecretKey: "", // bytedance + bytedanceUrl: DEFAULT_BYTEDANCE_URL, bytedanceApiKey: "", - bytedanceUrl: "", // alibaba - alibabaUrl: "", + alibabaUrl: DEFAULT_ALIBABA_URL, alibabaApiKey: "", // server config diff --git a/app/utils/cloudflare.ts b/app/utils/cloudflare.ts new file mode 100644 index 00000000000..5094640fcef --- /dev/null +++ b/app/utils/cloudflare.ts @@ -0,0 +1,26 @@ +export function cloudflareAIGatewayUrl(fetchUrl: string) { + // rebuild fetchUrl, if using cloudflare ai gateway + // document: https://developers.cloudflare.com/ai-gateway/providers/openai/ + + const paths = fetchUrl.split("/"); + if ("gateway.ai.cloudflare.com" == paths[2]) { + // is cloudflare.com ai gateway + // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/azure-openai/{resource_name}/{deployment_name}/chat/completions?api-version=2023-05-15' + if ("azure-openai" == paths[6]) { + // is azure gateway + return paths.slice(0, 8).concat(paths.slice(-3)).join("/"); // rebuild ai gateway azure_url + } + // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai/chat/completions + if ("openai" == paths[6]) { + // is openai gateway + return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway openai_url + } + // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic/v1/messages \ + if ("anthropic" == paths[6]) { + // is anthropic gateway + return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway anthropic_url + } + // TODO: Amazon Bedrock, Groq, HuggingFace... + } + return fetchUrl; +} diff --git a/app/utils/model.ts b/app/utils/model.ts index 8f6a1a6c786..55a5ee0d67d 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -1,9 +1,9 @@ import { DEFAULT_MODELS } from "../constant"; import { LLMModel } from "../client/api"; -const customProvider = (modelName: string) => ({ - id: modelName, - providerName: "Custom", +const customProvider = (providerName: string) => ({ + id: providerName.toLowerCase(), + providerName: providerName, providerType: "custom", }); @@ -71,10 +71,17 @@ export function collectModelTable( } // 2. if model not exists, create new model with available value if (count === 0) { - const provider = customProvider(name); - modelTable[`${name}@${provider?.id}`] = { - name, - displayName: displayName || name, + let [customModelName, customProviderName] = name.split("@"); + const provider = customProvider( + customProviderName || customModelName, + ); + // swap name and displayName for bytedance + if (displayName && provider.providerName == "ByteDance") { + [customModelName, displayName] = [displayName, customModelName]; + } + modelTable[`${customModelName}@${provider?.id}`] = { + name: customModelName, + displayName: displayName || customModelName, available, provider, // Use optional chaining }; diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 131a0810a0c..0967ef4666e 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ