ai-chatbot/lib/ai/zcode-agent/model-gateway.ts

77 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { getAgentAiModelWithProvider } from "@/lib/db/queries";
import { resolveModelRuntimeId } from "@/lib/ai/model-runtime";
import { selectVip98kChatInvocation } from "@/lib/ai/vip98k-chat-contract";
import { normalizeVip98kBaseUrl } from "@/lib/ai/vip98k-models";
type ConfiguredModel = NonNullable<Awaited<ReturnType<typeof getAgentAiModelWithProvider>>>;
const OPENAI_COMPATIBLE = new Set(["openai", "openai-compatible", "alibaba", "qwen", "local-qwen", "local", "zhipu", "moonshot", "deepseek", "xai", "siliconflow", "custom"]);
const catalogCache = new Map<string, { expires: number; value: unknown[] }>();
export async function createZcodeModelGateway(configured: ConfiguredModel, onError?: (message: string) => void) {
const { provider, model } = configured;
const modelId = resolveModelRuntimeId(model, { providerType: provider.type });
let base = String(provider.baseUrl || "").replace(/\/+$/, "");
let path = "/v1/chat/completions";
let apiType = "openai-chat-completions";
let contractHeaders: Record<string, string> = {};
if (provider.type === "vip98k") {
base = normalizeVip98kBaseUrl(base);
const capabilities = model.capabilities as Record<string, unknown> | null;
const imageConfig = model.imageGenConfig as Record<string, unknown> | null;
let contract = selectVip98kChatInvocation(capabilities?.vip98k || imageConfig?.vip98k);
if (!contract?.headers["X-Relay-Route-Option"]) {
let catalog = catalogCache.get(base);
if (!catalog || catalog.expires < Date.now()) {
const response = await fetch(`${base}/api/v2/catalog/models`, {
redirect: "error", signal: AbortSignal.timeout(12000), headers: { Accept: "application/json" },
});
if (!response.ok) throw new Error("Agent 模型调用规范暂不可用,请稍后重试");
const payload = await response.json();
const entries = (payload?.data?.sections || []).flatMap((section: { models?: unknown[] }) => section.models || []);
catalog = { expires: Date.now() + 5 * 60_000, value: entries };
if (catalogCache.size >= 16) catalogCache.delete(catalogCache.keys().next().value!);
catalogCache.set(base, catalog);
}
const entry = catalog.value.find(entry => (entry as { id?: string }).id === modelId);
contract = selectVip98kChatInvocation(entry) || contract;
}
if (!contract) throw new Error("当前 Agent 模型没有可用的对话调用规范");
path = contract.path; contractHeaders = contract.headers;
apiType = contract.protocol === "openai-responses" ? "openai-responses" : "openai-chat-completions";
} else {
if (!OPENAI_COMPATIBLE.has(provider.type)) throw new Error("当前 Agent 服务商尚未提供原生核心可用的对话协议");
base ||= provider.type === "openai" ? "https://api.openai.com" : "";
if (!base) throw new Error("Agent 服务商缺少接口地址");
base = base.replace(/\/v1$/, "");
}
const endpoint = new URL(base + path);
if (!["https:", "http:"].includes(endpoint.protocol)) throw new Error("Agent 服务商地址无效");
return {
modelId, apiType,
forward: async (body: Record<string, unknown>, requestedPath: string, signal: AbortSignal) => {
if (requestedPath !== path) return Response.json({ error: { message: "此任务的模型协议不匹配" } }, { status: 400 });
const headers: Record<string, string> = { "content-type": "application/json", ...contractHeaders };
if (apiType === "anthropic-messages") {
headers["x-api-key"] = provider.apiKey; headers["anthropic-version"] = "2023-06-01";
} else headers.Authorization = `Bearer ${provider.apiKey}`;
try {
const response = await fetch(endpoint, { method: "POST", headers, redirect: "error",
body: JSON.stringify({ ...body, model: modelId }),
signal: AbortSignal.any([signal, AbortSignal.timeout(120_000)]) });
if (response.ok) return response;
const message = response.status === 401 ? "当前 Agent 模型服务商鉴权失败,请检查该服务商密钥"
: response.status === 429 ? "当前 Agent 模型通道额度不足或请求过多,请稍后重试"
: `当前 Agent 模型通道暂不可用(${response.status})`;
onError?.(message);
await response.body?.cancel();
return Response.json({ error: { message, type: "canvas_model_gateway_error" } }, { status: response.status });
} catch {
const message = "Agent 模型连接中断,已保留任务记录"; onError?.(message);
return Response.json({ error: { message, type: "canvas_model_gateway_error" } }, { status: 502 });
}
},
};
}