diff --git a/app/api/tencent/[...path]/route.ts b/app/api/tencent/[...path]/route.ts index 1a5d42f85f6..8a292d432e2 100644 --- a/app/api/tencent/[...path]/route.ts +++ b/app/api/tencent/[...path]/route.ts @@ -1,3 +1,4 @@ +"use server"; import { getServerSideConfig } from "@/app/config/server"; import { TENCENT_BASE_URL, @@ -10,11 +11,7 @@ import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/api/auth"; import { isModelAvailableInServer } from "@/app/utils/model"; -import CryptoJS from "crypto-js"; -import mapKeys from "lodash-es/mapKeys"; -import mapValues from "lodash-es/mapValues"; -import isArray from "lodash-es/isArray"; -import isObject from "lodash-es/isObject"; +import * as crypto from "node:crypto"; const serverConfig = getServerSideConfig(); @@ -47,27 +44,6 @@ async function handle( export const GET = handle; export const POST = handle; -export const runtime = "edge"; -export const preferredRegion = [ - "arn1", - "bom1", - "cdg1", - "cle1", - "cpt1", - "dub1", - "fra1", - "gru1", - "hnd1", - "iad1", - "icn1", - "kix1", - "lhr1", - "pdx1", - "sfo1", - "sin1", - "syd1", -]; - async function request(req: NextRequest) { const controller = new AbortController(); @@ -99,63 +75,22 @@ async function request(req: NextRequest) { const fetchUrl = `${baseUrl}${path}`; - let body = null; - if (req.body) { - const bodyText = await req.text(); - console.log( - "Dogtiti ~ request ~ capitalizeKeys(JSON.parse(bodyText):", - capitalizeKeys(JSON.parse(bodyText)), - ); - body = JSON.stringify(capitalizeKeys(JSON.parse(bodyText))); - } - + const body = await req.text(); const fetchOptions: RequestInit = { headers: { ...getHeader(body), }, method: req.method, - body: '{"Model":"hunyuan-pro","Messages":[{"Role":"user","Content":"你好"}]}', // FIXME + body, redirect: "manual", // @ts-ignore duplex: "half", signal: controller.signal, }; - // #1815 try to refuse some request to some models - if (serverConfig.customModels && req.body) { - try { - const clonedBody = await req.text(); - fetchOptions.body = clonedBody; - - const jsonBody = JSON.parse(clonedBody) as { model?: string }; - - // not undefined and is false - if ( - isModelAvailableInServer( - serverConfig.customModels, - jsonBody?.model as string, - ServiceProvider.Tencent as string, - ) - ) { - return NextResponse.json( - { - error: true, - message: `you are not allowed to use ${jsonBody?.model} model`, - }, - { - status: 403, - }, - ); - } - } catch (e) { - console.error(`[Tencent] filter`, e); - } - } - console.log("[Tencent request]", fetchOptions.headers, req.method); try { const res = await fetch(fetchUrl, fetchOptions); - console.log("[Tencent response]", res.status, " ", res.headers, res.url); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate"); @@ -172,45 +107,16 @@ async function request(req: NextRequest) { } } -function capitalizeKeys(obj: any): any { - if (isArray(obj)) { - return obj.map(capitalizeKeys); - } else if (isObject(obj)) { - return mapValues( - mapKeys( - obj, - (value: any, key: string) => key.charAt(0).toUpperCase() + key.slice(1), - ), - capitalizeKeys, - ); - } else { - return obj; - } -} - // 使用 SHA-256 和 secret 进行 HMAC 加密 -function sha256(message: any, secret = "", encoding = "hex") { - const hmac = CryptoJS.HmacSHA256(message, secret); - if (encoding === "hex") { - return hmac.toString(CryptoJS.enc.Hex); - } else if (encoding === "base64") { - return hmac.toString(CryptoJS.enc.Base64); - } else { - return hmac.toString(); - } +function sha256(message: any, secret = "", encoding?: string) { + return crypto.createHmac("sha256", secret).update(message).digest(encoding); } // 使用 SHA-256 进行哈希 function getHash(message: any, encoding = "hex") { - const hash = CryptoJS.SHA256(message); - if (encoding === "hex") { - return hash.toString(CryptoJS.enc.Hex); - } else if (encoding === "base64") { - return hash.toString(CryptoJS.enc.Base64); - } else { - return hash.toString(); - } + return crypto.createHash("sha256").update(message).digest(encoding); } + function getDate(timestamp: number) { const date = new Date(timestamp * 1000); const year = date.getUTCFullYear(); @@ -238,10 +144,11 @@ function getHeader(payload: any) { const hashedRequestPayload = getHash(payload); const httpRequestMethod = "POST"; + const contentType = "application/json"; const canonicalUri = "/"; const canonicalQueryString = ""; const canonicalHeaders = - "content-type:application/json; charset=utf-8\n" + + `content-type:${contentType}\n` + "host:" + endpoint + "\n" + @@ -250,18 +157,14 @@ function getHeader(payload: any) { "\n"; const signedHeaders = "content-type;host;x-tc-action"; - const canonicalRequest = - httpRequestMethod + - "\n" + - canonicalUri + - "\n" + - canonicalQueryString + - "\n" + - canonicalHeaders + - "\n" + - signedHeaders + - "\n" + - hashedRequestPayload; + const canonicalRequest = [ + httpRequestMethod, + canonicalUri, + canonicalQueryString, + canonicalHeaders, + signedHeaders, + hashedRequestPayload, + ].join("\n"); // ************* 步骤 2:拼接待签名字符串 ************* const algorithm = "TC3-HMAC-SHA256"; @@ -299,7 +202,7 @@ function getHeader(payload: any) { return { Authorization: authorization, - "Content-Type": "application/json; charset=utf-8", + "Content-Type": contentType, Host: endpoint, "X-TC-Action": action, "X-TC-Timestamp": timestamp.toString(), diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index d812c5a82d5..82ecd316484 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -22,6 +22,10 @@ import { import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import { getMessageTextContent, isVisionModel } from "@/app/utils"; +import mapKeys from "lodash-es/mapKeys"; +import mapValues from "lodash-es/mapValues"; +import isArray from "lodash-es/isArray"; +import isObject from "lodash-es/isObject"; export interface OpenAIListModelResponse { object: string; @@ -33,17 +37,29 @@ export interface OpenAIListModelResponse { } interface RequestPayload { - messages: { - role: "system" | "user" | "assistant"; - content: string | MultimodalContent[]; + 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; + Stream?: boolean; + Model: string; + Temperature: number; + TopP: number; +} + +function capitalizeKeys(obj: any): any { + if (isArray(obj)) { + return obj.map(capitalizeKeys); + } else if (isObject(obj)) { + return mapValues( + mapKeys(obj, (value: any, key: string) => + key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()), + ), + capitalizeKeys, + ); + } else { + return obj; + } } export class HunyuanApi implements LLMApi { @@ -76,7 +92,7 @@ export class HunyuanApi implements LLMApi { } extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; + return res.Choices?.at(0)?.Message?.Content ?? ""; } async chat(options: ChatOptions) { @@ -94,15 +110,13 @@ export class HunyuanApi implements LLMApi { }, }; - const requestPayload: RequestPayload = { + const requestPayload: RequestPayload = capitalizeKeys({ messages, stream: options.config.stream, model: modelConfig.model, temperature: modelConfig.temperature, - presence_penalty: modelConfig.presence_penalty, - frequency_penalty: modelConfig.frequency_penalty, top_p: modelConfig.top_p, - }; + }); console.log("[Request] Tencent payload: ", requestPayload); @@ -213,10 +227,10 @@ export class HunyuanApi implements LLMApi { const text = msg.data; try { const json = JSON.parse(text); - const choices = json.choices as Array<{ - delta: { content: string }; + const choices = json.Choices as Array<{ + Delta: { Content: string }; }>; - const delta = choices[0]?.delta?.content; + const delta = choices[0]?.Delta?.Content; if (delta) { remainText += delta; } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index bde3a792ae7..086d36c99d2 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -54,6 +54,7 @@ import { Anthropic, Azure, Baidu, + Tencent, ByteDance, Alibaba, Google, @@ -964,6 +965,57 @@ export function Settings() { ); + const tencentConfigComponent = accessStore.provider === + ServiceProvider.Tencent && ( + <> + + + accessStore.update( + (access) => (access.tencentUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => (access.tencentApiKey = e.currentTarget.value), + ); + }} + /> + + + { + accessStore.update( + (access) => (access.tencentSecretKey = e.currentTarget.value), + ); + }} + /> + + + ); + const byteDanceConfigComponent = accessStore.provider === ServiceProvider.ByteDance && ( <> @@ -1364,6 +1416,7 @@ export function Settings() { {baiduConfigComponent} {byteDanceConfigComponent} {alibabaConfigComponent} + {tencentConfigComponent} {stabilityConfigComponent} )} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index cae41bfeeac..3127ac40b1e 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -371,6 +371,22 @@ const cn = { SubTitle: "不支持自定义前往.env配置", }, }, + Tencent: { + ApiKey: { + Title: "API Key", + SubTitle: "使用自定义腾讯云API Key", + Placeholder: "Tencent API Key", + }, + SecretKey: { + Title: "Secret Key", + SubTitle: "使用自定义腾讯云Secret Key", + Placeholder: "Tencent Secret Key", + }, + Endpoint: { + Title: "接口地址", + SubTitle: "不支持自定义前往.env配置", + }, + }, ByteDance: { ApiKey: { Title: "接口密钥", diff --git a/app/locales/en.ts b/app/locales/en.ts index bfb383e8f0f..0dc218031bf 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -354,6 +354,22 @@ const en: LocaleType = { SubTitle: "not supported, configure in .env", }, }, + Tencent: { + ApiKey: { + Title: "Tencent API Key", + SubTitle: "Use a custom Tencent API Key", + Placeholder: "Tencent API Key", + }, + SecretKey: { + Title: "Tencent Secret Key", + SubTitle: "Use a custom Tencent Secret Key", + Placeholder: "Tencent Secret Key", + }, + Endpoint: { + Title: "Endpoint Address", + SubTitle: "not supported, configure in .env", + }, + }, ByteDance: { ApiKey: { Title: "ByteDance API Key", diff --git a/app/store/access.ts b/app/store/access.ts index 288cf0a286e..af79480f9a0 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -39,6 +39,10 @@ const DEFAULT_ALIBABA_URL = isApp ? DEFAULT_API_HOST + "/api/proxy/alibaba" : ApiPath.Alibaba; +const DEFAULT_TENCENT_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/tencent" + : ApiPath.Tencent; + const DEFAULT_STABILITY_URL = isApp ? DEFAULT_API_HOST + "/api/proxy/stability" : ApiPath.Stability; @@ -87,7 +91,7 @@ const DEFAULT_ACCESS_STATE = { stabilityApiKey: "", // tencent - tencentUrl: "", + tencentUrl: DEFAULT_TENCENT_URL, tencentSecretKey: "", tencentSecretId: "",