TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import OpenAI from "openai";2import type { ResponseCreateParamsStreaming, Tool as ResponsesTool, ToolChoiceOptions } from "openai/resources/responses/responses";3import type { Model as OpenAIModel } from "openai/resources/models";4import {5 type AIProviderAdapter,6 type PolyModel,7 type UnifiedChatRequest,8 type UnifiedChatResponse,9 type UnifiedStreamEvent,10 type ValidationResult,11 type TokenEstimate,12 PolyProviderError,13 modelKey,14} from "@/lib/ai/core/types";15import { normalizeGenericError, refineByMessage, codeFromStatus, isRetryableStatus, parseRetryAfter } from "@/lib/ai/core/errors";16import { filterSettings, heuristicTokens } from "@/lib/ai/core/normalize";17import { collectStream } from "@/lib/ai/core/stream-utils";18import { toResponsesInput, normalizeResponsesStream } from "../shared/openai-compat/responses";19import { OPENAI_CATALOG, OPENAI_NON_CHAT, OPENAI_DEAD_IDS } from "./catalog";2021const DEFAULT_TIMEOUT_MS = 10 * 60_000;2223function client(apiKey: string, timeoutMs = DEFAULT_TIMEOUT_MS) {24 return new OpenAI({ apiKey, maxRetries: 1, timeout: timeoutMs });25}2627type ModelWithShutdown = OpenAIModel & { shutdown_date?: string | null };2829export function normalizeOpenAIModel(m: ModelWithShutdown): PolyModel | null {30 if (OPENAI_NON_CHAT.test(m.id) || OPENAI_DEAD_IDS.has(m.id)) return null;31 if (!/^(gpt-|o[1-9]|chatgpt|chat-latest)/.test(m.id)) return null;32 const shutdown = m.shutdown_date ?? null;33 if (shutdown && new Date(shutdown).getTime() < Date.now()) return null;34 const cat = OPENAI_CATALOG.get(m.id);35 const reasoningGuess = /^(o[1-9]|gpt-5|gpt-6)/.test(m.id);36 return {37 key: modelKey("openai", m.id),38 id: m.id,39 provider: "openai",40 displayName: cat?.displayName ?? m.id,41 family: cat?.family ?? m.id.split("-").slice(0, 2).join("-"),42 capabilities: cat?.capabilities ?? {43 text: true,44 vision: true,45 audioInput: false,46 audioOutput: false,47 imageGeneration: false,48 video: false,49 reasoning: reasoningGuess,50 tools: true,51 structuredOutput: true,52 streaming: true,53 files: true,54 webSearch: true,55 },56 limits: cat?.limits ?? {},57 parameters: cat?.parameters ?? { temperature: !reasoningGuess, topP: !reasoningGuess, maxTokens: true, stop: false, seed: false },58 status: cat?.status ?? "unknown",59 pricing: cat?.pricing ?? null,60 metadata: { ...(cat?.metadata ?? {}), shutdownDate: shutdown, createdAt: m.created ? new Date(m.created * 1000).toISOString() : undefined, sortWeight: cat?.sortWeight ?? 0, verified: Boolean(cat) },61 };62}6364function buildParams(req: UnifiedChatRequest): ResponseCreateParamsStreaming {65 const { settings } = filterSettings(req.settings, req.modelInfo);66 const meta = (req.modelInfo?.metadata ?? {}) as Record<string, unknown>;67 const reasoningCapable = req.modelInfo?.capabilities.reasoning ?? /^(o[1-9]|gpt-5|gpt-6)/.test(req.model);68 const effort = settings.reasoningEffort;69 const params: ResponseCreateParamsStreaming = {70 model: req.model,71 input: toResponsesInput(req.messages, { nativeFiles: true, replayReasoning: true }),72 stream: true,73 store: false,74 truncation: "auto",75 };76 if (req.system?.trim()) params.instructions = req.system;77 if (settings.maxTokens !== undefined) params.max_output_tokens = Math.max(16, settings.maxTokens);7879 // Sampling: only for non-reasoning models, or reasoning models explicitly at effort "none".80 const samplingMode = (meta.samplingMode as string | undefined) ?? (reasoningCapable ? "conditional" : "yes");81 const samplingOk = samplingMode === "yes" || (samplingMode === "conditional" && effort === "none");82 if (samplingOk) {83 if (settings.temperature !== undefined) params.temperature = settings.temperature;84 if (settings.topP !== undefined) params.top_p = settings.topP;85 if (settings.frequencyPenalty !== undefined) (params as unknown as Record<string, unknown>).frequency_penalty = settings.frequencyPenalty;86 if (settings.presencePenalty !== undefined) (params as unknown as Record<string, unknown>).presence_penalty = settings.presencePenalty;87 }8889 if (reasoningCapable) {90 params.reasoning = {};91 if (effort) params.reasoning.effort = effort as NonNullable<ResponseCreateParamsStreaming["reasoning"]>["effort"];92 if (settings.includeReasoning !== false && effort !== "none") params.reasoning.summary = "auto";93 params.include = ["reasoning.encrypted_content"];94 }95 if (settings.verbosity) params.text = { ...(params.text ?? {}), verbosity: settings.verbosity };96 if (settings.responseFormat?.type === "json_schema" && settings.responseFormat.schema) {97 params.text = { ...(params.text ?? {}), format: { type: "json_schema", name: settings.responseFormat.schemaName ?? "response", schema: settings.responseFormat.schema, strict: settings.responseFormat.strict ?? true } };98 } else if (settings.responseFormat?.type === "json") {99 params.text = { ...(params.text ?? {}), format: { type: "json_object" } };100 }101102 const tools: ResponsesTool[] = [];103 if (settings.webSearch) tools.push({ type: "web_search" } as ResponsesTool);104 if (settings.codeExecution) tools.push({ type: "code_interpreter", container: { type: "auto" } } as ResponsesTool);105 for (const t of req.tools ?? []) tools.push({ type: "function", name: t.name, description: t.description, parameters: t.parameters, strict: t.strict ?? true });106 if (tools.length) {107 params.tools = tools;108 const tc = settings.toolChoice;109 if (tc === "none" || tc === "required" || tc === "auto") params.tool_choice = tc as ToolChoiceOptions;110 else if (tc && typeof tc === "object") params.tool_choice = { type: "function", name: tc.name };111 }112 return params;113}114115export const openaiAdapter: AIProviderAdapter = {116 id: "openai",117 name: "OpenAI",118 keyDocsUrl: "https://platform.openai.com/api-keys",119 keyPrefixHint: "sk-",120121 async validateApiKey(apiKey, signal): Promise<ValidationResult> {122 const t0 = Date.now();123 try {124 const page = await client(apiKey, 20_000).models.list({ signal });125 let n = 0;126 for await (const m of page) if (normalizeOpenAIModel(m as ModelWithShutdown)) n++;127 return { ok: true, modelsAvailable: n, latencyMs: Date.now() - t0 };128 } catch (e) {129 return { ok: false, error: this.normalizeError(e).toJSON(), latencyMs: Date.now() - t0 };130 }131 },132133 async listModels(apiKey, signal): Promise<PolyModel[]> {134 try {135 const out: PolyModel[] = [];136 for await (const m of client(apiKey, 30_000).models.list({ signal })) {137 const n = normalizeOpenAIModel(m as ModelWithShutdown);138 if (n) out.push(n);139 }140 // `gpt-5.6` is a documented alias (→ gpt-5.6-sol) that /v1/models does not list.141 if (out.some((m) => m.id === "gpt-5.6-sol") && !out.some((m) => m.id === "gpt-5.6")) {142 const alias = normalizeOpenAIModel({ id: "gpt-5.6", object: "model", created: 0, owned_by: "openai" });143 if (alias) out.push(alias);144 }145 return out;146 } catch (e) {147 throw this.normalizeError(e);148 }149 },150151 async chat(req): Promise<UnifiedChatResponse> {152 return collectStream("openai", req.model, this.streamChat(req));153 },154155 async *streamChat(req: UnifiedChatRequest): AsyncIterable<UnifiedStreamEvent> {156 try {157 const stream = await client(req.apiKey, req.timeoutMs).responses.create(buildParams(req), { signal: req.signal });158 yield* normalizeResponsesStream(stream, "openai");159 } catch (e) {160 yield { type: "error", error: this.normalizeError(e).toJSON() };161 }162 },163164 async estimateTokens(req): Promise<TokenEstimate> {165 try {166 const params = buildParams(req);167 const res = await client(req.apiKey, 20_000).responses.inputTokens.count({ model: params.model, input: params.input, instructions: params.instructions, tools: params.tools, reasoning: params.reasoning });168 return { inputTokens: res.input_tokens, method: "provider" };169 } catch {170 const text = req.messages.map((m) => m.content.map((p) => (p.type === "text" ? p.text : "")).join(" ")).join(" ") + (req.system ?? "");171 return { inputTokens: heuristicTokens(text), method: "heuristic" };172 }173 },174175 normalizeError(error: unknown): PolyProviderError {176 if (error instanceof PolyProviderError) return error;177 if (error instanceof OpenAI.APIError) {178 const code = (error.code ?? undefined) as string | undefined;179 const param = (error as { param?: string | null }).param ?? undefined;180 const message = error.message ?? "OpenAI error";181 let poly = codeFromStatus(error.status);182 if (code === "invalid_api_key" || error.status === 401) poly = "INVALID_API_KEY";183 else if (code === "model_not_found") poly = "MODEL_NOT_FOUND";184 else if (code === "insufficient_quota") poly = "INSUFFICIENT_CREDITS";185 else if (code === "context_length_exceeded") poly = "CONTEXT_TOO_LONG";186 else if (code === "rate_limit_exceeded" || code === "slow_down") poly = "RATE_LIMITED";187 else if (code === "server_is_overloaded") poly = "PROVIDER_UNAVAILABLE";188 else if (code === "unsupported_parameter" || code === "unsupported_value" || code === "unknown_parameter" || code === "integer_below_min_value" || code === "invalid_type" || /unsupported parameter/i.test(message)) poly = "INVALID_PARAMETER";189 else if (error.status === 400) poly = refineByMessage("INVALID_PARAMETER", message);190 return new PolyProviderError({191 code: poly,192 message: poly === "INVALID_API_KEY" ? "Invalid API key" : message.slice(0, 600),193 provider: "openai",194 status: error.status,195 retryable: isRetryableStatus(error.status) && poly !== "INSUFFICIENT_CREDITS",196 retryAfterMs: parseRetryAfter(error.headers ?? null),197 providerCode: code ?? (param ? `param:${param}` : undefined),198 cause: error,199 });200 }201 return normalizeGenericError("openai", error);202 },203};204