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: "",