import "server-only"; import { AgentModelProtocolError, createAgentModelFailover, type AgentCircuitStore, type AgentModelEntry } from "@/lib/ai/runtime/agent-model-failover"; import { agentModelCircuitStore } from "@/lib/ai/runtime/agent-model-circuit-store"; import { toResponsesRequest, responsesCompletion, completionResponse } from "./model-gateway-protocol"; import { resolveModelRuntimeId } from "@/lib/ai/model-runtime"; import { selectVip98kChatInvocation } from "@/lib/ai/vip98k-chat-contract"; import { normalizeVip98kBaseUrl } from "@/lib/ai/vip98k-models"; type ConfiguredModel = AgentModelEntry; const OPENAI_COMPATIBLE = new Set(["openai", "openai-compatible", "alibaba", "qwen", "local-qwen", "local", "zhipu", "moonshot", "deepseek", "xai", "siliconflow", "custom"]); const catalogCache = new Map(); async function resolveGateway(configured: ConfiguredModel) { 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 = {}; if (provider.type === "vip98k") { base = normalizeVip98kBaseUrl(base); const capabilities = model.capabilities as Record | null; const imageConfig = model.imageGenConfig as Record | 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", "User-Agent": "Mozilla/5.0" }, }); if (!response.ok) throw Object.assign(new Error("Agent catalog unavailable"), { status: response.status }); 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 AgentModelProtocolError(); 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, endpoint, contractHeaders, apiKey: provider.apiKey }; } export async function createZcodeModelGateway(configured: ConfiguredModel, onError?: (message: string) => void, candidates: AgentModelEntry[] = [configured], circuit: AgentCircuitStore = agentModelCircuitStore) { const run = createAgentModelFailover(candidates, circuit); // The native core sees one stable chat protocol. Responses-only provider // routes are translated behind this boundary, including tool history. return { modelId: resolveModelRuntimeId(configured.model, { providerType: configured.provider.type }), apiType: "openai-chat-completions", forward: async (body: Record, requestedPath: string, signal: AbortSignal) => { if (requestedPath !== "/v1/chat/completions") return Response.json({ error: { message: "此任务的模型协议不匹配" } }, { status: 400 }); try { const completion = await run(async (entry, attemptSignal) => { const target = await resolveGateway(entry); attemptSignal.throwIfAborted(); const request = target.apiType === "openai-responses" ? toResponsesRequest(body) : { ...body }; delete request.stream_options; const response = await fetch(target.endpoint, { method: "POST", redirect: "error", headers: { "content-type": "application/json", ...target.contractHeaders, Authorization: `Bearer ${target.apiKey}` }, body: JSON.stringify({ ...request, model: target.modelId, stream: false }), signal: attemptSignal }); if (!response.ok) { // Use the body only to distinguish input/policy rejection from a // provider outage. It is never persisted, logged or sent to clients. const text = (await response.text()).slice(0, 4096); throw Object.assign(new Error(text || "upstream request failed"), { status: response.status }); } let payload: Record; try { payload = await response.json(); } catch { throw new AgentModelProtocolError(); } if (target.apiType === "openai-responses") payload = responsesCompletion(payload); return completionResponse(payload, body); }, signal); return completion; } catch (error) { if (signal.aborted) throw error; const message = "Agent 模型通道暂不可用,已保留执行记录,请稍后重试"; onError?.(message); return Response.json({ error: { message, type: "canvas_model_gateway_error" } }, { status: 503 }); } }, }; }