77 lines
4.6 KiB
TypeScript
77 lines
4.6 KiB
TypeScript
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 });
|
||
}
|
||
},
|
||
};
|
||
}
|