-
Notifications
You must be signed in to change notification settings - Fork 59.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: [#5714] 支持GLM #5741
feat: [#5714] 支持GLM #5741
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,129 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { getServerSideConfig } from "@/app/config/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CHATGLM_BASE_URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ApiPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ModelProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ServiceProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} from "@/app/constant"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { prettyObject } from "@/app/utils/format"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { auth } from "@/app/api/auth"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { isModelAvailableInServer } from "@/app/utils/model"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const serverConfig = getServerSideConfig(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export async function handle( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
req: NextRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ params }: { params: { path: string[] } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log("[GLM Route] params ", params); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (req.method === "OPTIONS") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json({ body: "OK" }, { status: 200 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const authResult = auth(req, ModelProvider.GLM); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (authResult.error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json(authResult, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
status: 401, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const response = await request(req); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return response; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+32
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error cleanup in catch block. The try-catch block should clean up any resources (like response streams) in case of an error to prevent memory leaks. Apply this diff: try {
const response = await request(req);
return response;
} catch (e) {
+ if (response?.body) {
+ try {
+ await response.body.cancel();
+ } catch {} // Ignore cleanup errors
+ }
console.error("[GLM] ", e);
return NextResponse.json(prettyObject(e));
}
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error("[GLM] ", e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json(prettyObject(e)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+36
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid Exposing Internal Error Details to Clients Returning the full error object in the API response may reveal sensitive internal information. It's better to return a generic error message to the client and log the detailed error on the server side. Apply this diff to return a generic error response: console.error("[GLM] ", e);
- return NextResponse.json(prettyObject(e));
+ return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function request(req: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const controller = new AbortController(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// alibaba use base url or just remove the path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!baseUrl.startsWith("http")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
baseUrl = `https://${baseUrl}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+47
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add URL validation for security. The base URL construction should validate the URL to prevent potential SSRF attacks. Apply this diff: + function isValidUrl(url: string): boolean {
+ try {
+ const parsedUrl = new URL(url);
+ return ['http:', 'https:'].includes(parsedUrl.protocol);
+ } catch {
+ return false;
+ }
+ }
let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
+ if (!isValidUrl(baseUrl)) {
+ throw new Error("Invalid base URL configuration");
+ } 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (baseUrl.endsWith("/")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
baseUrl = baseUrl.slice(0, -1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log("[Proxy] ", path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log("[Base Url]", baseUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const timeoutId = setTimeout( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
controller.abort(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
10 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const fetchUrl = `${baseUrl}${path}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log("[Fetch Url] ", fetchUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const fetchOptions: RequestInit = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Authorization: req.headers.get("Authorization") ?? "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
method: req.method, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
body: req.body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
redirect: "manual", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+75
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure Request Body Consistency When Reading Streams The request body ( To fix this, read the body once and reuse it: - body: req.body,
+ const clonedBody = await req.text();
+ body: clonedBody,
...
if (serverConfig.customModels && clonedBody) {
try {
- const clonedBody = await req.text();
- fetchOptions.body = clonedBody;
+ fetchOptions.body = clonedBody; Also applies to: 85-86 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// @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.ChatGLM as string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
error: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
message: `you are not allowed to use ${jsonBody?.model} model`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
status: 403, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+91
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix inverted logic in model availability check. The condition for returning a 403 response is incorrect. It returns 403 when the model IS available, which is the opposite of what's intended. Apply this diff: if (
- isModelAvailableInServer(
+ !isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.ChatGLM as string,
)
) { 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error(`[GLM] filter`, e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+108
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling in model validation. The catch block silently logs errors from JSON parsing or model validation. These should be properly handled as they indicate invalid requests. Apply this diff: } catch (e) {
console.error(`[GLM] filter`, e);
+ return NextResponse.json(
+ {
+ error: true,
+ message: "Invalid request format",
+ },
+ {
+ status: 400,
+ }
+ );
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const res = await fetch(fetchUrl, fetchOptions); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// to prevent browser prompt for credentials | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const newHeaders = new Headers(res.headers); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
newHeaders.delete("www-authenticate"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// to disable nginx buffering | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
newHeaders.set("X-Accel-Buffering", "no"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return new Response(res.body, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
status: res.status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
statusText: res.statusText, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
headers: newHeaders, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
clearTimeout(timeoutId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import { HunyuanApi } from "./platforms/tencent"; | |
import { MoonshotApi } from "./platforms/moonshot"; | ||
import { SparkApi } from "./platforms/iflytek"; | ||
import { XAIApi } from "./platforms/xai"; | ||
import { ChatGLMApi } from "./platforms/glm"; | ||
|
||
export const ROLES = ["system", "user", "assistant"] as const; | ||
export type MessageRole = (typeof ROLES)[number]; | ||
|
@@ -156,6 +157,9 @@ export class ClientApi { | |
case ModelProvider.XAI: | ||
this.llm = new XAIApi(); | ||
break; | ||
case ModelProvider.ChatGLM: | ||
this.llm = new ChatGLMApi(); | ||
break; | ||
default: | ||
this.llm = new ChatGPTApi(); | ||
} | ||
|
@@ -244,6 +248,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { | |
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot; | ||
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek; | ||
const isXAI = modelConfig.providerName === ServiceProvider.XAI; | ||
const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider refactoring the nested ternary operators While the ChatGLM integration is functionally correct, the nested ternary operators for API key selection are becoming increasingly complex and harder to maintain. Consider refactoring this logic to improve readability. Here's a suggested refactor using a more maintainable approach: function getApiKeyForProvider(
modelConfig: ModelConfig,
accessStore: AccessStore
): string {
const providerMap = {
[ServiceProvider.Google]: accessStore.googleApiKey,
[ServiceProvider.Azure]: accessStore.azureApiKey,
[ServiceProvider.Anthropic]: accessStore.anthropicApiKey,
[ServiceProvider.ByteDance]: accessStore.bytedanceApiKey,
[ServiceProvider.Alibaba]: accessStore.alibabaApiKey,
[ServiceProvider.Moonshot]: accessStore.moonshotApiKey,
[ServiceProvider.XAI]: accessStore.xaiApiKey,
[ServiceProvider.ChatGLM]: accessStore.chatglmApiKey,
[ServiceProvider.Iflytek]: accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? `${accessStore.iflytekApiKey}:${accessStore.iflytekApiSecret}`
: "",
// Default to OpenAI
default: accessStore.openaiApiKey,
};
return providerMap[modelConfig.providerName] ?? providerMap.default;
} This approach:
Also applies to: 267-268, 284-284 |
||
const isEnabledAccessControl = accessStore.enabledAccessControl(); | ||
const apiKey = isGoogle | ||
? accessStore.googleApiKey | ||
|
@@ -259,6 +264,8 @@ export function getHeaders(ignoreHeaders: boolean = false) { | |
? accessStore.moonshotApiKey | ||
: isXAI | ||
? accessStore.xaiApiKey | ||
: isChatGLM | ||
? accessStore.chatglmApiKey | ||
: isIflytek | ||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret | ||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret | ||
|
@@ -274,6 +281,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { | |
isMoonshot, | ||
isIflytek, | ||
isXAI, | ||
isChatGLM, | ||
apiKey, | ||
isEnabledAccessControl, | ||
}; | ||
|
@@ -338,6 +346,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { | |
return new ClientApi(ModelProvider.Iflytek); | ||
case ServiceProvider.XAI: | ||
return new ClientApi(ModelProvider.XAI); | ||
case ServiceProvider.ChatGLM: | ||
return new ClientApi(ModelProvider.ChatGLM); | ||
default: | ||
return new ClientApi(ModelProvider.GPT); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set CORS Headers for Preflight Requests
When handling
OPTIONS
requests for CORS preflight, it's important to include the appropriate headers to allow cross-origin requests. Currently, the response lacks CORS headers, which may cause client requests to fail due to CORS policy restrictions.Apply this diff to include the necessary CORS headers:
📝 Committable suggestion