import { createOpenAI } from "@ai-sdk/openai"; import { APICallError, type LanguageModelV2, type LanguageModelV2CallOptions } from "@ai-sdk/provider"; import { selectVip98kChatInvocation } from "../vip98k-chat-contract"; const contractCache = new Map>; }>(); function discoverContracts(root: string, baseFetch: typeof fetch) { const cached = contractCache.get(root); if (cached && cached.expiresAt > Date.now()) return cached.promise; const entry = { expiresAt: Date.now() + 5 * 60_000, promise: (async () => { // This is a public catalog: never attach the model API key to discovery. const response = await baseFetch(`${root}/api/v2/catalog/models`, { headers: { Accept: "application/json" }, redirect: "error", signal: AbortSignal.timeout(12_000), cache: "no-store", }); if (!response.ok) throw new Error("VIP98K 调用规范获取失败,请稍后同步模型目录。"); const payload = record(await response.json()); const data = record(payload?.data); const sections = Array.isArray(data?.sections) ? data.sections : []; const models = new Map(); for (const section of sections) { const items = record(section)?.models; for (const item of Array.isArray(items) ? items : []) { const model = record(item); if (model?.id) models.set(String(model.id), { chatInvocation: selectVip98kChatInvocation(model), }); } } if (!models.size) throw new Error("VIP98K 未返回可用的调用规范,请同步模型目录。"); return models; })(), }; if (contractCache.size >= 32) contractCache.delete(contractCache.keys().next().value!); contractCache.set(root, entry); // Do not cache errors or delete a newer entry when an older fetch fails. entry.promise.catch(() => { if (contractCache.get(root) === entry) contractCache.delete(root); }); return entry.promise; } async function awaitWithAbort(promise: Promise, signal?: AbortSignal): Promise { if (!signal) return promise; signal.throwIfAborted(); return new Promise((resolve, reject) => { const onAbort = () => reject(signal.reason); signal.addEventListener("abort", onAbort, { once: true }); promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort)); }); } function record(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function emptyResponseError() { return new APICallError({ message: "VIP98K 未返回可用的回答或工具调用,请检查模型通道及输出额度;未自动重复提交。", url: "", requestBodyValues: undefined, isRetryable: false, }); } function requiresToolCall(options: LanguageModelV2CallOptions) { return options.toolChoice?.type === "required" || options.toolChoice?.type === "tool"; } function missingToolError() { return new APICallError({ message: "VIP98K 通道未返回所要求的工具调用,无法执行画布操作;请检查该通道的工具支持。", url: "", requestBodyValues: undefined, isRetryable: false, }); } function createChatFetch(baseURL: string, baseFetch: typeof fetch): typeof fetch { const origin = new URL(baseURL).origin; return async (input, init) => { const url = new URL(input instanceof Request ? input.url : String(input)); if (url.origin !== origin) throw new Error("VIP98K 对话请求不能跨服务商发送密钥"); const response = await baseFetch(input, { ...init, redirect: "error" }); if (response.ok) return response; // Do not pass provider HTML, echoed prompts or credentials to chat/logs. let code = ""; let outcomeUnknown = false; try { const body = record(await response.json()); const error = record(body?.error); code = String(error?.code || ""); outcomeUnknown = code === "request_outcome_unknown" || /final outcome could not be confirmed/i.test(String(error?.message || "")); } catch { /* A proxy may return an HTML error page. */ } const message = outcomeUnknown ? "VIP98K 已接收请求但无法确认结果,已停止自动重试,避免重复调用和扣费。" : response.status === 401 ? "VIP98K 密钥鉴权失败,请检查服务商密钥。" : code === "model_protocol_unsupported" ? "VIP98K 模型调用协议不匹配,请同步该模型的最新接口配置。" : `VIP98K 模型请求失败(HTTP ${response.status}),请检查通道状态。`; throw new APICallError({ message, url: `${url.origin}${url.pathname}`, requestBodyValues: undefined, statusCode: response.status, // Paid POSTs may already have reached the upstream. Retrying a 503 with // a new request identity can create duplicate charges. isRetryable: false, }); }; } export function createVip98kChatModel(params: { apiKey: string; baseURL: string; modelId: string; metadata?: unknown; fetch?: typeof fetch; discoverLegacyContract?: boolean; }): LanguageModelV2 { const metadata = record(params.metadata) || {}; const invocation = selectVip98kChatInvocation(metadata); const hasContract = Boolean(metadata.invocation || metadata.chatInvocation || (Array.isArray(metadata.invocations) && metadata.invocations.length)); if (hasContract && !invocation && !params.discoverLegacyContract) { throw new Error("VIP98K 此模型未公布兼容的对话接口,请核对模型类型和接口配置。"); } const root = params.baseURL.replace(/\/+$/, "").replace(/\/v1$/i, ""); const baseFetch = params.fetch || fetch; const createModel = (contract: typeof invocation) => { const provider = createOpenAI({ apiKey: params.apiKey, baseURL: `${root}/v1`, headers: contract?.headers, fetch: createChatFetch(root, baseFetch), }); return contract?.protocol === "openai-responses" ? provider.responses(params.modelId) : provider.chat(params.modelId); }; const model = createModel(invocation); const needsDiscovery = params.discoverLegacyContract && !invocation?.headers["X-Relay-Route-Option"]; const resolveModel = async (signal?: AbortSignal) => { signal?.throwIfAborted(); if (!needsDiscovery) return model; const models = await awaitWithAbort(discoverContracts(root, baseFetch), signal); const contract = selectVip98kChatInvocation(models.get(params.modelId)); if (!contract) throw new Error("VIP98K 此模型未公布兼容的对话接口,请核对模型类型和接口配置。"); return createModel(contract); }; return { specificationVersion: model.specificationVersion, provider: model.provider, modelId: model.modelId, supportedUrls: model.supportedUrls, async doGenerate(options) { const activeModel = await resolveModel(options.abortSignal); const result = await activeModel.doGenerate(options); if (requiresToolCall(options) && !result.content.some((part) => part.type === "tool-call")) throw missingToolError(); if (!result.content.some((part) => part.type === "tool-call" || (part.type === "text" && part.text.trim()))) throw emptyResponseError(); return result; }, async doStream(options) { const activeModel = await resolveModel(options.abortSignal); const result = await activeModel.doStream(options); let usable = false; let toolCalled = false; let failed = false; let finished = false; return { ...result, stream: result.stream.pipeThrough(new TransformStream({ transform(part, controller) { if (part.type === "tool-call") toolCalled = true; if (part.type === "tool-call" || (part.type === "text-delta" && part.delta.trim())) usable = true; if (part.type === "error") failed = true; if (part.type === "finish") { finished = true; if (!failed && part.finishReason === "unknown") { throw new Error("VIP98K 响应连接提前中断,结果未确认;未自动重复提交。"); } if (!failed && requiresToolCall(options) && !toolCalled) throw missingToolError(); if (!usable && !failed) throw emptyResponseError(); } controller.enqueue(part); }, flush() { if (!failed && !finished) { throw new Error("VIP98K 响应连接提前中断,结果未确认;未自动重复提交。"); } }, })) }; }, }; }