Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
1.7 MiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
{"repo": "can1357/oh-my-pi", "files": {"packages/catalog/src/provider-models/openai-compat.ts": {"8baa3300bc23c721fc80c0307f0b7a8218be8fcb": {"t": 1785595144.745103, "n": 206142, "h": "1952270ad698ac0f", "c": "import { VERSION } from \"@oh-my-pi/pi-utils\";\nimport * as logger from \"@oh-my-pi/pi-utils/logger\";\nimport {\n\tfetchOpenAICompatibleModels,\n\ttype OpenAICompatibleModelMapperContext,\n\ttype OpenAICompatibleModelRecord,\n} from \"../discovery/openai-compatible\";\nimport { Effort, THINKING_EFFORTS } from \"../effort\";\nimport { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from \"../fireworks-model-id\";\nimport { getBundledModelReferenceIndex } from \"../identity/bundled\";\nimport {\n\tanthropicModelSupportsThinking,\n\tisGlmVisionModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiModelId,\n\tisReasoningGlmModelId,\n} from \"../identity/family\";\nimport { resolveModelReference } from \"../identity/reference\";\nimport type { ModelManagerOptions } from \"../model-manager\";\nimport { getBundledModels } from \"../models\";\nimport type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig } from \"../types\";\nimport { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from \"../utils\";\nimport { ALIBABA_TOKEN_PLAN_BASE_URL, parseAlibabaTokenPlanCredential } from \"../wire/alibaba-token-plan\";\nimport { coreWeaveProjectHeaders } from \"../wire/coreweave\";\nimport {\n\tCOPILOT_API_HEADERS,\n\tgetGitHubCopilotBaseUrl,\n\tisPersonalGitHubCopilotBaseUrl,\n\tparseGitHubCopilotApiKey,\n} from \"../wire/github-copilot\";\nimport { createBundledReferenceMap, createReferenceResolver, toModelSpec } from \"./bundled-references\";\nimport { getDefaultModelDiscoveryBaseUrl, resolveModelCacheProviderId } from \"./cache-provider-id\";\n\nconst MODELS_DEV_URL = \"https://catalog.stencil.so/models.json.zstd\";\n\n/** Little-endian magic number opening every zstd frame (RFC 8878). */\nconst ZSTD_MAGIC = 0xfd2fb528;\n\n/**\n * Uses a cancellable timer rather than the native abort-timeout helper so\n * successful fast discovery requests do not leave armed timeout signals for\n * concurrent GC to trip over later.\n */\nasync function withCatalogDiscoveryTimeout<T>(timeoutMs: number, run: (signal: AbortSignal) => Promise<T>): Promise<T> {\n\tconst controller = new AbortController();\n\tconst timer = setTimeout(\n\t\t() => controller.abort(new DOMException(\"The operation timed out.\", \"TimeoutError\")),\n\t\ttimeoutMs,\n\t);\n\ttry {\n\t\treturn await run(controller.signal);\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\nconst ANTHROPIC_BASE_URL = \"https://api.anthropic.com/v1\";\nconst ANTHROPIC_OAUTH_BETA =\n\t\"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24,extended-cache-ttl-2025-04-11\";\n\nexport interface ModelsDevModel {\n\tid?: string;\n\tname?: string;\n\ttool_call?: boolean;\n\treasoning?: boolean;\n\tlimit?: {\n\t\tcontext?: number;\n\t\toutput?: number;\n\t};\n\tcost?: {\n\t\tinput?: number;\n\t\toutput?: number;\n\t\tcache_read?: number;\n\t\tcache_write?: number;\n\t};\n\tmodalities?: {\n\t\tinput?: string[];\n\t};\n\tstatus?: string;\n\tprovider?: { npm?: string };\n}\n\nfunction toModelName(value: unknown, fallback: string): string {\n\tif (typeof value !== \"string\") {\n\t\treturn fallback;\n\t}\n\tconst trimmed = value.trim();\n\treturn trimmed.length > 0 ? trimmed : fallback;\n}\n\nfunction toInputCapabilities(value: unknown): (\"text\" | \"image\")[] {\n\tif (!Array.isArray(value)) {\n\t\treturn [\"text\"];\n\t}\n\tconst supportsImage = value.some(item => item === \"image\");\n\treturn supportsImage ? [\"text\", \"image\"] : [\"text\"];\n}\n\n/**\n * Process-wide catalog session: the first call downloads the payload (the one\n * request the server logs); later calls revalidate with `If-None-Match` and\n * reuse the decoded payload on `304`. Failure after a successful load falls\n * back to the session copy.\n */\nconst catalogSession: {\n\tinflight: Promise<unknown> | null;\n\tpayload: unknown;\n\tetag: string | null;\n\thasPayload: boolean;\n} = { inflight: null, payload: undefined, etag: null, hasPayload: false };\n\nconst CATALOG_USER_AGENT = `omp/${VERSION} (+https://omp.sh)`;\n\n/**\n * Fetches the models.dev catalog via catalog.stencil.so, which serves a\n * field-pruned copy precompressed as a zstd blob (~93 KB vs ~3.3 MB raw).\n * The frame magic is sniffed rather than trusting content-type so plain-JSON\n * responses (test stubs, fallback mirrors) parse identically.\n *\n * Fetched fully once per process: concurrent callers share the in-flight\n * request, repeat callers send a conditional GET that the server answers\n * (and deliberately does not log) with `304`.\n */\nexport function fetchWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<unknown> {\n\tif (!catalogSession.inflight) {\n\t\tcatalogSession.inflight = fetchCatalogPayload(fetchImpl ?? discoveryFetch(), signal).finally(() => {\n\t\t\tcatalogSession.inflight = null;\n\t\t});\n\t}\n\treturn catalogSession.inflight;\n}\n\nasync function fetchCatalogPayload(fetchImpl: FetchImpl, signal?: AbortSignal): Promise<unknown> {\n\tconst headers: Record<string, string> = {\n\t\tAccept: \"application/zstd, application/json\",\n\t\t\"User-Agent\": CATALOG_USER_AGENT,\n\t};\n\tif (catalogSession.hasPayload && catalogSession.etag) {\n\t\theaders[\"If-None-Match\"] = catalogSession.etag;\n\t}\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetchImpl(MODELS_DEV_URL, { method: \"GET\", headers, signal });\n\t} catch (error) {\n\t\tif (catalogSession.hasPayload) {\n\t\t\treturn catalogSession.payload;\n\t\t}\n\t\tthrow error;\n\t}\n\tif (response.status === 304 && catalogSession.hasPayload) {\n\t\treturn catalogSession.payload;\n\t}\n\tif (!response.ok) {\n\t\tif (catalogSession.hasPayload) {\n\t\t\treturn catalogSession.payload;\n\t\t}\n\t\tthrow new Error(`models catalog fetch failed: ${response.status}`);\n\t}\n\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\tconst isZstd = bytes.length >= 4 && new DataView(bytes.buffer, bytes.byteOffset).getUint32(0, true) === ZSTD_MAGIC;\n\tconst text = new TextDecoder().decode(isZstd ? await Bun.zstdDecompress(bytes) : bytes);\n\tconst payload: unknown = JSON.parse(text);\n\tcatalogSession.payload = payload;\n\tcatalogSession.etag = response.headers.get(\"etag\");\n\tcatalogSession.hasPayload = true;\n\treturn payload;\n}\n\nfunction mapAnthropicModelsDev(payload: unknown, baseUrl: string): ModelSpec<\"anthropic-messages\">[] {\n\tif (!isRecord(payload)) {\n\t\treturn [];\n\t}\n\tconst anthropicPayload = payload.anthropic;\n\tif (!isRecord(anthropicPayload)) {\n\t\treturn [];\n\t}\n\tconst modelsValue = anthropicPayload.models;\n\tif (!isRecord(modelsValue)) {\n\t\treturn [];\n\t}\n\n\tconst models: ModelSpec<\"anthropic-messages\">[] = [];\n\tfor (const [modelId, rawModel] of Object.entries(modelsValue)) {\n\t\tif (!isRecord(rawModel)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst model = rawModel as ModelsDevModel;\n\t\tif (model.tool_call !== true) {\n\t\t\tcontinue;\n\t\t}\n\t\tmodels.push({\n\t\t\tid: modelId,\n\t\t\tname: toModelName(model.name, modelId),\n\t\t\tapi: \"anthropic-messages\",\n\t\t\tprovider: \"anthropic\",\n\t\t\tbaseUrl,\n\t\t\treasoning: model.reasoning === true,\n\t\t\tinput: toInputCapabilities(model.modalities?.input),\n\t\t\tcost: {\n\t\t\t\tinput: toNumber(model.cost?.input) ?? 0,\n\t\t\t\toutput: toNumber(model.cost?.output) ?? 0,\n\t\t\t\tcacheRead: toNumber(model.cost?.cache_read) ?? 0,\n\t\t\t\tcacheWrite: toNumber(model.cost?.cache_write) ?? 0,\n\t\t\t},\n\t\t\tcontextWindow: toPositiveNumber(model.limit?.context, null),\n\t\t\tmaxTokens: toPositiveNumber(model.limit?.output, null),\n\t\t});\n\t}\n\n\tmodels.sort((left, right) => left.id.localeCompare(right.id));\n\treturn models;\n}\n\nfunction buildAnthropicDiscoveryHeaders(apiKey: string): Record<string, string> {\n\tconst oauthToken = isAnthropicOAuthToken(apiKey);\n\tconst headers: Record<string, string> = {\n\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\"anthropic-beta\": ANTHROPIC_OAUTH_BETA,\n\t};\n\tif (oauthToken) {\n\t\theaders.Authorization = `Bearer ${apiKey}`;\n\t} else {\n\t\theaders[\"x-api-key\"] = apiKey;\n\t}\n\treturn headers;\n}\n\nfunction buildAnthropicReferenceMap(\n\tmodelsDevModels: readonly ModelSpec<\"anthropic-messages\">[],\n): Map<string, ModelSpec<\"anthropic-messages\">> {\n\tconst merged = new Map<string, ModelSpec<\"anthropic-messages\">>();\n\tfor (const model of modelsDevModels) {\n\t\tmerged.set(model.id, model);\n\t}\n\t// Anthropic /v1/models does not carry token limits, so bundled metadata stays canonical\n\t// for known models while models.dev only fills gaps for newly discovered ids.\n\tconst bundledModels = getBundledModels(\"anthropic\").filter(\n\t\t(model): model is Model<\"anthropic-messages\"> => model.api === \"anthropic-messages\",\n\t);\n\tfor (const model of bundledModels) {\n\t\tmerged.set(model.id, toModelSpec(model));\n\t}\n\treturn merged;\n}\n\n/**\n * Curated Anthropic models that are live or limited-availability on the\n * first-party `/v1/models` endpoint but that models.dev has not catalogued yet.\n * Seeded into model generation so the bundled catalog is never gated on\n * models.dev's update cadence; deduped behind upstream catalog / models.dev\n * entries once those appear. Token limits and pricing are pinned either directly or\n * in `applyAnthropicCatalogPolicy`, and `thinking` is re-baked\n * by the generator's policy pass (scripts/generated-policies.ts).\n */\nexport const ANTHROPIC_CURATED_FALLBACK_MODELS: readonly ModelSpec<\"anthropic-messages\">[] = [\n\t{\n\t\tid: \"claude-sonnet-5\",\n\t\tname: \"Claude Sonnet 5\",\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"anthropic\",\n\t\tbaseUrl: \"https://api.anthropic.com\",\n\t\treasoning: true,\n\t\tinput: [\"text\", \"image\"],\n\t\tcost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 128_000,\n\t},\n\t{\n\t\tid: \"claude-fable-5\",\n\t\tname: \"Claude Fable 5\",\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"anthropic\",\n\t\tbaseUrl: \"https://api.anthropic.com\",\n\t\treasoning: true,\n\t\tinput: [\"text\", \"image\"],\n\t\tcost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 128_000,\n\t},\n\t{\n\t\tid: \"claude-mythos-5\",\n\t\tname: \"Claude Mythos 5\",\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"anthropic\",\n\t\tbaseUrl: \"https://api.anthropic.com\",\n\t\treasoning: true,\n\t\tinput: [\"text\", \"image\"],\n\t\tcost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 128_000,\n\t},\n];\n\nfunction mapWithBundledReference<TApi extends Api>(\n\tentry: OpenAICompatibleModelRecord,\n\tdefaults: ModelSpec<TApi>,\n\treference: ModelSpec<TApi> | undefined,\n): ModelSpec<TApi> {\n\tconst name = toModelName(entry.name, reference?.name ?? defaults.name);\n\tif (!reference) {\n\t\treturn {\n\t\t\t...defaults,\n\t\t\tname,\n\t\t};\n\t}\n\treturn {\n\t\t...reference,\n\t\tid: defaults.id,\n\t\tname,\n\t\tapi: defaults.api,\n\t\tprovider: defaults.provider,\n\t\tbaseUrl: defaults.baseUrl,\n\t\tcontextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),\n\t\tmaxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),\n\t};\n}\n\nfunction normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {\n\tconst value = baseUrl?.trim();\n\tif (!value) {\n\t\treturn fallback;\n\t}\n\treturn value.endsWith(\"/\") ? value.slice(0, -1) : value;\n}\n\nfunction toAnthropicDiscoveryBaseUrl(baseUrl: string): string {\n\treturn baseUrl.endsWith(\"/v1\") ? baseUrl : `${baseUrl}/v1`;\n}\n\nfunction normalizeOllamaBaseUrl(baseUrl?: string): string {\n\tconst value = baseUrl?.trim();\n\tif (!value) {\n\t\treturn \"http://127.0.0.1:11434/v1\";\n\t}\n\tconst trimmed = value.endsWith(\"/\") ? value.slice(0, -1) : value;\n\treturn trimmed.endsWith(\"/v1\") ? trimmed : `${trimmed}/v1`;\n}\n\nfunction toOllamaNativeBaseUrl(baseUrl: string): string {\n\treturn baseUrl.endsWith(\"/v1\") ? baseUrl.slice(0, -3) : baseUrl;\n}\n\nasync function fetchOllamaNativeModels(\n\tbaseUrl: string,\n\tresolveMetadata: (modelId: string) => Promise<OllamaResolvedMetadata>,\n\tfetchImpl: FetchImpl = discoveryFetch(),\n): Promise<ModelSpec<\"openai-responses\">[] | null> {\n\tconst nativeBaseUrl = toOllamaNativeBaseUrl(baseUrl);\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetchImpl(`${nativeBaseUrl}/api/tags`, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: { Accept: \"application/json\" },\n\t\t});\n\t} catch {\n\t\treturn null;\n\t}\n\tif (!response.ok) {\n\t\treturn null;\n\t}\n\tconst payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };\n\tconst entries = payload.models ?? [];\n\tconst resolved = await Promise.all(\n\t\tentries.map(async (entry): Promise<ModelSpec<\"openai-responses\"> | null> => {\n\t\t\tconst id = entry.model ?? entry.name;\n\t\t\tif (!id) return null;\n\t\t\tconst metadata = await resolveMetadata(id);\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tname: entry.name ?? id,\n\t\t\t\tapi: \"openai-responses\",\n\t\t\t\tprovider: \"ollama\",\n\t\t\t\tbaseUrl,\n\t\t\t\treasoning: metadata.reasoning ?? false,\n\t\t\t\tthinking: metadata.thinking,\n\t\t\t\tinput: metadata.input ?? [\"text\"],\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: metadata.contextWindow,\n\t\t\t\tmaxTokens: metadata.maxTokens,\n\t\t\t};\n\t\t}),\n\t);\n\tconst models: ModelSpec<\"openai-responses\">[] = resolved.filter(\n\t\t(m): m is ModelSpec<\"openai-responses\"> => m !== null,\n\t);\n\treturn models.sort((left, right) => left.id.localeCompare(right.id));\n}\n\n/**\n * Fallback context window for Ollama models when `/api/show` is unavailable\n * or omits a `model_info.<arch>.context_length` field. Matches the size\n * Ollama's cloud catalog reports for stock models.\n */\nconst OLLAMA_FALLBACK_CONTEXT_WINDOW = 128_000;\n/** Cap max output tokens at a value that matches OMP's other openai-responses defaults. */\nconst OLLAMA_DEFAULT_MAX_TOKENS = 8192;\n\ninterface OllamaResolvedMetadata {\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tcapabilities?: string[];\n\treasoning?: boolean;\n\tthinking?: ThinkingConfig;\n\tinput?: (\"text\" | \"image\")[];\n}\n\ninterface OllamaShowMetadata {\n\tcontextWindow?: number;\n\tmaxTokens?: number;\n\tcapabilities?: string[];\n\treasoning?: boolean;\n\tthinking?: ThinkingConfig;\n\tinput?: (\"text\" | \"image\")[];\n}\n\nfunction getOllamaContextWindow(modelInfo: Record<string, unknown> | undefined): number | undefined {\n\tif (!modelInfo) {\n\t\treturn undefined;\n\t}\n\tfor (const [key, value] of Object.entries(modelInfo)) {\n\t\tif (typeof value !== \"number\" || value <= 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (key.endsWith(\".context_length\") || key.endsWith(\".num_ctx\") || key.endsWith(\".context_window\")) {\n\t\t\treturn value;\n\t\t}\n\t}\n}\n\nfunction getOllamaCapabilities(value: unknown): string[] | undefined {\n\tif (!Array.isArray(value)) {\n\t\treturn undefined;\n\t}\n\treturn value.filter((item): item is string => typeof item === \"string\");\n}\n\nfunction getOllamaThinkingConfig(capabilities: string[] | undefined): ThinkingConfig | undefined {\n\tif (!capabilities?.includes(\"thinking\")) {\n\t\treturn undefined;\n\t}\n\treturn { mode: \"effort\", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] };\n}\n\n/**\n * Query Ollama's `/api/show` endpoint for a single model and pull native\n * context and capability metadata from the response. Returns `undefined` when\n * the endpoint is unavailable so callers can layer their own fallback.\n */\nasync function fetchOllamaShowMetadata(\n\tnativeBaseUrl: string,\n\tmodelId: string,\n\tfetchImpl: FetchImpl = discoveryFetch(),\n): Promise<OllamaShowMetadata | undefined> {\n\ttry {\n\t\tconst response = await fetchImpl(`${nativeBaseUrl}/api/show`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\", Accept: \"application/json\" },\n\t\t\tbody: JSON.stringify({ model: modelId }),\n\t\t});\n\t\tif (!response.ok) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst payload = (await response.json()) as { capabilities?: unknown; model_info?: Record<string, unknown> };\n\t\tconst capabilities = getOllamaCapabilities(payload.capabilities);\n\t\tconst contextWindow = getOllamaContextWindow(payload.model_info);\n\t\treturn {\n\t\t\tcontextWindow,\n\t\t\tmaxTokens: contextWindow ? OLLAMA_DEFAULT_MAX_TOKENS : undefined,\n\t\t\tcapabilities,\n\t\t\treasoning: capabilities ? capabilities.includes(\"thinking\") : undefined,\n\t\t\tthinking: getOllamaThinkingConfig(capabilities),\n\t\t\tinput: capabilities\n\t\t\t\t? capabilities.includes(\"vision\")\n\t\t\t\t\t? ([\"text\", \"image\"] as Array<\"text\" | \"image\">)\n\t\t\t\t\t: ([\"text\"] as Array<\"text\">)\n\t\t\t\t: undefined,\n\t\t};\n\t} catch {\n\t\t// fall through; caller decides on the fallback\n\t}\n\treturn undefined;\n}\n\n/**\n * Build a resolver that fetches `/api/show` metadata per model id and caches\n * the result in-memory for the lifetime of the manager. Successful lookups are\n * cached so repeated `fetchDynamicModels` calls do not refetch; failed\n * lookups stay uncached so a later refresh can recover.\n */\nfunction createOllamaMetadataResolver(\n\tnativeBaseUrl: string,\n\tfetchImpl?: FetchImpl,\n): (modelId: string) => Promise<OllamaResolvedMetadata> {\n\tconst cache = new Map<string, Promise<OllamaResolvedMetadata>>();\n\treturn modelId => {\n\t\tconst cached = cache.get(modelId);\n\t\tif (cached) return cached;\n\t\tconst pending = (async () => {\n\t\t\tconst metadata = await fetchOllamaShowMetadata(nativeBaseUrl, modelId, fetchImpl);\n\t\t\tif (!metadata) {\n\t\t\t\tcache.delete(modelId);\n\t\t\t\treturn { contextWindow: OLLAMA_FALLBACK_CONTEXT_WINDOW, maxTokens: OLLAMA_DEFAULT_MAX_TOKENS };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t...metadata,\n\t\t\t\tcontextWindow: metadata.contextWindow ?? OLLAMA_FALLBACK_CONTEXT_WINDOW,\n\t\t\t\tmaxTokens: metadata.maxTokens ?? OLLAMA_DEFAULT_MAX_TOKENS,\n\t\t\t};\n\t\t})();\n\t\tcache.set(modelId, pending);\n\t\tvoid pending.catch(() => cache.delete(modelId));\n\t\treturn pending;\n\t};\n}\n\nconst OPENAI_NON_RESPONSES_PREFIXES = [\n\t\"text-embedding\",\n\t\"whisper-\",\n\t\"tts-\",\n\t\"omni-moderation\",\n\t\"omni-transcribe\",\n\t\"omni-speech\",\n\t\"gpt-image-\",\n\t\"gpt-realtime\",\n] as const;\n\nfunction isLikelyOpenAIResponsesModelId(id: string, references: Map<string, ModelSpec<\"openai-responses\">>): boolean {\n\tconst trimmed = id.trim();\n\tif (!trimmed) {\n\t\treturn false;\n\t}\n\tif (references.has(trimmed)) {\n\t\treturn true;\n\t}\n\tconst normalized = trimmed.toLowerCase();\n\tif (OPENAI_NON_RESPONSES_PREFIXES.some(prefix => normalized.startsWith(prefix))) {\n\t\treturn false;\n\t}\n\tif (normalized.includes(\"embedding\")) {\n\t\treturn false;\n\t}\n\treturn (\n\t\tnormalized.startsWith(\"gpt-\") ||\n\t\tnormalized.startsWith(\"o1\") ||\n\t\tnormalized.startsWith(\"o3\") ||\n\t\tnormalized.startsWith(\"o4\") ||\n\t\tnormalized.startsWith(\"chatgpt\")\n\t);\n}\n\nconst NANO_GPT_NON_TEXT_MODEL_TOKENS = [\n\t\"embedding\",\n\t\"image\",\n\t\"vision\",\n\t\"audio\",\n\t\"speech\",\n\t\"transcribe\",\n\t\"moderation\",\n\t\"realtime\",\n\t\"whisper\",\n\t\"tts\",\n] as const;\n\n/** Regex matching NanoGPT `:thinking` suffixed model IDs (with or without a level). */\nconst NANO_GPT_THINKING_SUFFIX_RE = /:thinking(:[^:]+)?$/;\n\nfunction isLikelyNanoGptTextModelId(id: string): boolean {\n\tconst normalized = id.trim().toLowerCase();\n\tif (!normalized) {\n\t\treturn false;\n\t}\n\tif (NANO_GPT_THINKING_SUFFIX_RE.test(normalized)) {\n\t\treturn false;\n\t}\n\treturn !NANO_GPT_NON_TEXT_MODEL_TOKENS.some(token => normalized.includes(token));\n}\n\ntype SimpleProviderDiscoveryHeaders = Record<string, string> | (() => Record<string, string> | undefined);\ntype SimpleProviderConfig = {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n\theaders?: SimpleProviderDiscoveryHeaders;\n};\n\nfunction resolveSimpleProviderHeaders(\n\theaders: SimpleProviderDiscoveryHeaders | undefined,\n): Record<string, string> | undefined {\n\treturn typeof headers === \"function\" ? headers() : headers;\n}\n\nexport function createSimpleOpenAICompletionsOptions(\n\tproviderId: Parameters<typeof getBundledModels>[0],\n\tdefaultBaseUrl: string,\n\tconfig?: SimpleProviderConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? defaultBaseUrl;\n\tconst references = createBundledReferenceMap<\"openai-completions\">(providerId);\n\treturn {\n\t\tproviderId,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: providerId,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders: resolveSimpleProviderHeaders(config?.headers),\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\treturn mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\nfunction createSimpleOpenAIResponsesOptions(\n\tproviderId: Parameters<typeof getBundledModels>[0],\n\tdefaultBaseUrl: string,\n\tconfig?: SimpleProviderConfig,\n): ModelManagerOptions<\"openai-responses\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? defaultBaseUrl;\n\tconst references = createBundledReferenceMap<\"openai-responses\">(providerId);\n\treturn {\n\t\tproviderId,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-responses\",\n\t\t\t\t\tprovider: providerId,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders: resolveSimpleProviderHeaders(config?.headers),\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\treturn mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\nfunction createSimpleAnthropicProviderOptions(\n\tproviderId: Parameters<typeof getBundledModels>[0],\n\tdefaultBaseUrlFallback: string,\n\tconfig?: SimpleProviderConfig,\n): ModelManagerOptions<\"anthropic-messages\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = normalizeAnthropicBaseUrl(config?.baseUrl, defaultBaseUrlFallback);\n\tconst discoveryBaseUrl = toAnthropicDiscoveryBaseUrl(baseUrl);\n\tconst references = createBundledReferenceMap<\"anthropic-messages\">(providerId);\n\treturn {\n\t\tproviderId,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"anthropic-messages\",\n\t\t\t\t\tprovider: providerId,\n\t\t\t\t\tbaseUrl: discoveryBaseUrl,\n\t\t\t\t\theaders: buildAnthropicDiscoveryHeaders(apiKey),\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\tconst model = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...model,\n\t\t\t\t\t\t\tname: toModelName(entry.display_name, model.name),\n\t\t\t\t\t\t\tbaseUrl,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Umans AI Coding Plan\n// ---------------------------------------------------------------------------\n\nconst UMANS_BASE_URL = \"https://api.code.umans.ai\";\nconst UMANS_MODELS_INFO_PATH = \"/models/info\";\nconst UMANS_REASONING_EFFORT_BY_LEVEL: Record<string, Effort> = {\n\tminimal: Effort.Minimal,\n\tlow: Effort.Low,\n\tmedium: Effort.Medium,\n\thigh: Effort.High,\n\txhigh: Effort.XHigh,\n\tmax: Effort.Max,\n};\nconst UMANS_DEFAULT_REASONING_EFFORTS = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh] as const;\nconst UMANS_VIA_HANDOFF_MODEL_IDS = [\"umans-glm-5.1\", \"umans-glm-5.2\"] as const;\n\nexport interface UmansModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\ninterface UmansModelInfo {\n\tname?: unknown;\n\tdisplay_name?: unknown;\n\tcapabilities?: unknown;\n}\n\nfunction normalizeUmansBaseUrl(baseUrl: string | undefined): string {\n\tconst normalized = normalizeAnthropicBaseUrl(baseUrl, UMANS_BASE_URL);\n\treturn normalized.endsWith(\"/v1\") ? normalized.slice(0, -3) : normalized;\n}\n\n/**\n * Umans `models/info` reports `supports_vision: true` for natively\n * vision-capable models and a non-empty string sentinel (e.g.\n * `\"via-handoff\"`) for models that route image inputs through a vision\n * handoff pre-analysis step instead of accepting raw image blocks. Only\n * `true` means the model accepts image content directly; sentinel values\n * MUST map to text-only so the agent's vision-handoff path runs instead\n * of triggering an upstream HTTP 400 (`This model does not support image\n * inputs`).\n */\nfunction umansSupportsVision(value: unknown): boolean {\n\treturn value === true;\n}\n\nfunction umansReasoningSupported(value: unknown): boolean {\n\treturn isRecord(value) ? value.supported === true : value === true;\n}\n\nfunction mapUmansReasoningEfforts(value: unknown): readonly Effort[] {\n\tif (!isRecord(value) || !Array.isArray(value.levels)) {\n\t\treturn UMANS_DEFAULT_REASONING_EFFORTS;\n\t}\n\tconst efforts: Effort[] = [];\n\tfor (const level of value.levels) {\n\t\tif (typeof level !== \"string\") continue;\n\t\tconst effort = UMANS_REASONING_EFFORT_BY_LEVEL[level];\n\t\tif (effort !== undefined && !efforts.includes(effort)) {\n\t\t\tefforts.push(effort);\n\t\t}\n\t}\n\treturn efforts.length > 0 ? efforts : UMANS_DEFAULT_REASONING_EFFORTS;\n}\n\nfunction umansHasMaxReasoningLevel(value: unknown): boolean {\n\treturn isRecord(value) && Array.isArray(value.levels) && value.levels.includes(\"max\");\n}\n\nfunction mapUmansThinkingConfig(value: unknown): ThinkingConfig | undefined {\n\tif (!umansReasoningSupported(value)) return undefined;\n\tconst efforts = mapUmansReasoningEfforts(value);\n\tconst thinking: ThinkingConfig = {\n\t\tmode: umansHasMaxReasoningLevel(value) ? \"anthropic-budget-effort\" : \"budget\",\n\t\tefforts,\n\t};\n\tif (isRecord(value)) {\n\t\tif (value.can_disable === false) {\n\t\t\tthinking.requiresEffort = true;\n\t\t}\n\t\tif (typeof value.default_level === \"string\") {\n\t\t\tconst defaultLevel = UMANS_REASONING_EFFORT_BY_LEVEL[value.default_level];\n\t\t\tif (defaultLevel !== undefined && efforts.includes(defaultLevel)) {\n\t\t\t\tthinking.defaultLevel = defaultLevel;\n\t\t\t}\n\t\t}\n\t}\n\treturn thinking;\n}\n\nfunction mapUmansModelInfo(\n\tmodelId: string,\n\traw: UmansModelInfo,\n\tbaseUrl: string,\n\treference: ModelSpec<\"anthropic-messages\"> | undefined,\n): ModelSpec<\"anthropic-messages\"> | null {\n\tif (!modelId) return null;\n\tconst capabilities = isRecord(raw.capabilities) ? raw.capabilities : {};\n\tconst supportsTools = capabilities.supports_tools;\n\tconst thinking = mapUmansThinkingConfig(capabilities.reasoning);\n\treturn {\n\t\t...reference,\n\t\tid: modelId,\n\t\tname: toModelName(raw.display_name, toModelName(raw.name, modelId)),\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"umans\",\n\t\tbaseUrl,\n\t\tcompat: { ...reference?.compat, escapeBuiltinToolNames: true },\n\t\treasoning: thinking !== undefined,\n\t\t...(thinking ? { thinking } : {}),\n\t\tinput: umansSupportsVision(capabilities.supports_vision) ? [\"text\", \"image\"] : [\"text\"],\n\t\t...(supportsTools === false ? { supportsTools: false } : {}),\n\t\tcost: reference?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: toPositiveNumber(capabilities.context_window, reference?.contextWindow ?? null),\n\t\tmaxTokens: toPositiveNumber(\n\t\t\tcapabilities.recommended_max_tokens,\n\t\t\ttoPositiveNumber(capabilities.max_completion_tokens, reference?.maxTokens ?? null),\n\t\t),\n\t};\n}\n\nasync function fetchUmansModelsInfo(options: {\n\tbaseUrl: string;\n\tapiKey?: string;\n\tfetch?: FetchImpl;\n\treferences: Map<string, ModelSpec<\"anthropic-messages\">>;\n}): Promise<ModelSpec<\"anthropic-messages\">[] | null> {\n\tconst discoveryBaseUrl = toAnthropicDiscoveryBaseUrl(options.baseUrl);\n\tconst requestHeaders: Record<string, string> = { Accept: \"application/json\" };\n\tif (options.apiKey) {\n\t\trequestHeaders[\"x-api-key\"] = options.apiKey;\n\t}\n\tconst fetchImpl = discoveryFetch(options.fetch);\n\tlet payload: unknown;\n\ttry {\n\t\tconst response = await fetchImpl(`${discoveryBaseUrl}${UMANS_MODELS_INFO_PATH}`, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: requestHeaders,\n\t\t});\n\t\tif (!response.ok) {\n\t\t\treturn null;\n\t\t}\n\t\tpayload = await response.json();\n\t} catch (error) {\n\t\tthrow new Error(\"Failed to fetch Umans models info\", { cause: error });\n\t}\n\tif (!isRecord(payload)) {\n\t\treturn null;\n\t}\n\tconst models: ModelSpec<\"anthropic-messages\">[] = [];\n\tfor (const [modelId, value] of Object.entries(payload)) {\n\t\tif (!isRecord(value)) continue;\n\t\tconst mapped = mapUmansModelInfo(modelId, value, options.baseUrl, options.references.get(modelId));\n\t\tif (mapped) {\n\t\t\tmodels.push(mapped);\n\t\t}\n\t}\n\treturn models.sort((left, right) => left.id.localeCompare(right.id));\n}\n\nexport function umansModelManagerOptions(config?: UmansModelManagerConfig): ModelManagerOptions<\"anthropic-messages\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = normalizeUmansBaseUrl(config?.baseUrl);\n\tconst references = createBundledReferenceMap<\"anthropic-messages\">(\"umans\");\n\treturn {\n\t\tproviderId: \"umans\",\n\t\tdynamicModelsAuthoritative: true,\n\t\tdropCachedModelIdsOnStaticMismatch: UMANS_VIA_HANDOFF_MODEL_IDS,\n\t\tfetchDynamicModels: () => fetchUmansModelsInfo({ baseUrl, apiKey, fetch: config?.fetch, references }),\n\t};\n}\n// ---------------------------------------------------------------------------\n// 1. OpenAI\n// ---------------------------------------------------------------------------\n\nexport interface OpenAIModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<\"openai-responses\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.openai.com/v1\";\n\tconst references = createBundledReferenceMap<\"openai-responses\">(\"openai\");\n\treturn {\n\t\tproviderId: \"openai\",\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-responses\",\n\t\t\t\t\tprovider: \"openai\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tfilterModel: (_entry, model) => isLikelyOpenAIResponsesModelId(model.id, references),\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\treturn mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n/** First-party gpt-5.6 SKUs that accept `reasoning: { mode: \"pro\" }` on the Responses APIs. */\nconst OPENAI_PRO_REASONING_BASE_IDS: Record<string, true> = {\n\t\"gpt-5.6-luna\": true,\n\t\"gpt-5.6-sol\": true,\n\t\"gpt-5.6-terra\": true,\n};\n/**\n * Providers whose generated pro aliases this pass owns. `openai-codex` stays in\n * the sweep so stale aliases from earlier snapshots are dropped on regen, but\n * projection is `openai`-only — subscription (Codex) auth does not offer pro\n * reasoning.\n */\nconst OPENAI_PRO_REASONING_SWEEP_PROVIDERS: Record<string, true> = { openai: true, \"openai-codex\": true };\n\n/**\n * A row this generator pass owns: one of the derived `gpt-5.6-*-pro` alias ids\n * on a swept provider that carries the generated `reasoningMode` marker.\n * A real upstream model occupying the same id has no `reasoningMode` and is\n * never touched.\n */\nfunction isGeneratedOpenAIProReasoningAlias(model: ModelSpec<Api>): boolean {\n\treturn (\n\t\tOPENAI_PRO_REASONING_SWEEP_PROVIDERS[model.provider] === true &&\n\t\tmodel.reasoningMode !== undefined &&\n\t\tmodel.id.endsWith(\"-pro\") &&\n\t\tOPENAI_PRO_REASONING_BASE_IDS[model.id.slice(0, -\"-pro\".length)] === true\n\t);\n}\n\n/**\n * Re-derive the generated pro-reasoning aliases (`gpt-5.6-*-pro`) for the\n * first-party `openai` gpt-5.6 rows. Each alias inherits the base row's\n * metadata, requests the base wire id via `requestModelId`, and sets\n * `reasoningMode: \"pro\"` so Responses-family request builders emit\n * `reasoning: { mode: \"pro\" }`. Called by the models.json generator after all\n * sources merge: stale copies of the owned aliases (previous snapshot,\n * including retired `openai-codex` rows) are dropped and re-projected from the\n * current base rows so alias metadata always tracks the base, while a real\n * upstream model that occupies an alias id wins and suppresses the projection.\n */\nexport function projectOpenAIProReasoningAliases(models: readonly ModelSpec<Api>[]): ModelSpec<Api>[] {\n\tconst kept = models.filter(model => !isGeneratedOpenAIProReasoningAlias(model));\n\tconst ids = new Set(kept.map(model => `${model.provider}/${model.id}`));\n\tconst out = [...kept];\n\tfor (const model of kept) {\n\t\tif (model.provider !== \"openai\") continue;\n\t\tif (!OPENAI_PRO_REASONING_BASE_IDS[model.id]) continue;\n\t\tconst aliasId = `${model.id}-pro`;\n\t\tconst aliasKey = `${model.provider}/${aliasId}`;\n\t\tif (ids.has(aliasKey)) continue;\n\t\tids.add(aliasKey);\n\t\tout.push({\n\t\t\t...model,\n\t\t\tid: aliasId,\n\t\t\tname: `${model.name} Pro`,\n\t\t\trequestModelId: model.id,\n\t\t\treasoningMode: \"pro\",\n\t\t});\n\t}\n\treturn out;\n}\n\n// ---------------------------------------------------------------------------\n// 1b. GMI Cloud\n// ---------------------------------------------------------------------------\n\nconst GMI_CLOUD_BASE_URL = \"https://api.gmi-serving.com/v1\";\n\n/**\n * Bundled seed for GMI Cloud. Generation has no `GMI_API_KEY`, so a regen\n * without credentials would leave the provider slice empty and the declared\n * `defaultModel` unresolvable on a fresh install before the async runtime\n * discovery fires. Live `/v1/models` discovery is authoritative for the model\n * ID set and overrides context/max-token limits, but `mapWithBundledReference`\n * keeps the reference's cost/reasoning/thinking — so these fields carry GMI's\n * direct-tariff values: V4-Flash at $0.14/$0.28 per 1M with Think High/Max\n * modes per GMI's launch post\n * (https://www.gmicloud.ai/en/blog/deepseek-v4-is-here-we-tested-it), not\n * discounted gateway-route pricing. GMI publishes no cache-read tariff, so\n * cacheRead stays 0 until a direct source confirms cached-token billing.\n */\nexport const GMI_CLOUD_STATIC_MODELS: readonly ModelSpec<\"openai-completions\">[] = [\n\t{\n\t\tid: \"deepseek-ai/DeepSeek-V4-Flash\",\n\t\tname: \"DeepSeek V4 Flash\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"gmi-cloud\",\n\t\tbaseUrl: GMI_CLOUD_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\"],\n\t\tcost: { input: 0.14, output: 0.28, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1048576,\n\t\tmaxTokens: 384000,\n\t\tthinking: { mode: \"effort\", efforts: [Effort.High, Effort.Max] },\n\t},\n];\n\nexport interface GmiCloudModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function gmiCloudModelManagerOptions(\n\tconfig?: GmiCloudModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"gmi-cloud\", GMI_CLOUD_BASE_URL, config);\n}\n\n// ---------------------------------------------------------------------------\n// 2. Groq\n// ---------------------------------------------------------------------------\n\nexport interface GroqModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function groqModelManagerOptions(config?: GroqModelManagerConfig): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"groq\", \"https://api.groq.com/openai/v1\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 3. Cerebras\n// ---------------------------------------------------------------------------\n\nconst CEREBRAS_IMAGE_INPUT_MODEL_IDS = new Set([\"gemma-4-31b\"]);\n\nfunction applyCerebrasDiscoveryOverrides(model: ModelSpec<\"openai-completions\">): ModelSpec<\"openai-completions\"> {\n\tif (!CEREBRAS_IMAGE_INPUT_MODEL_IDS.has(model.id)) {\n\t\treturn model;\n\t}\n\treturn {\n\t\t...model,\n\t\tinput: [\"text\", \"image\"],\n\t};\n}\n\nexport interface CerebrasModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function cerebrasModelManagerOptions(\n\tconfig?: CerebrasModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.cerebras.ai/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"cerebras\");\n\treturn {\n\t\tproviderId: \"cerebras\",\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"cerebras\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\tconst model = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t\treturn applyCerebrasDiscoveryOverrides(model);\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 4. Hugging Face\n// ---------------------------------------------------------------------------\n\nexport interface HuggingfaceModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function huggingfaceModelManagerOptions(\n\tconfig?: HuggingfaceModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"huggingface\", \"https://router.huggingface.co/v1\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 5. NVIDIA\n// ---------------------------------------------------------------------------\n\nexport interface NvidiaModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function nvidiaModelManagerOptions(\n\tconfig?: NvidiaModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"nvidia\", \"https://integrate.api.nvidia.com/v1\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 5.5 Novita\n// ---------------------------------------------------------------------------\n\n/** Novita OpenAI-compatible discovery configuration. */\nexport interface NovitaModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nfunction novitaArrayIncludes(value: unknown, expected: string): boolean {\n\treturn Array.isArray(value) && value.some(item => item === expected);\n}\n\nfunction isPublicNovitaModelId(id: string): boolean {\n\treturn !id.toLowerCase().startsWith(\"ai_infer_test\");\n}\n\n// Novita reports token prices in 1/10,000 USD per million tokens.\nfunction toNovitaCostPerMillion(value: unknown): number {\n\treturn toPositiveNumber(value, 0) / 10_000;\n}\n\nfunction getNovitaCacheReadPricePerMillion(entry: OpenAICompatibleModelRecord): number {\n\tconst pricing = entry.pricing;\n\tif (!isRecord(pricing)) {\n\t\treturn 0;\n\t}\n\tconst cacheRead = pricing.input_cache_read;\n\tif (!isRecord(cacheRead)) {\n\t\treturn 0;\n\t}\n\treturn toNovitaCostPerMillion(cacheRead.price_per_m);\n}\n\nfunction mapNovitaModel(\n\tentry: OpenAICompatibleModelRecord,\n\tdefaults: ModelSpec<\"openai-completions\">,\n\treference: ModelSpec<\"openai-completions\"> | undefined,\n): ModelSpec<\"openai-completions\"> {\n\tconst model = mapWithBundledReference(\n\t\t{\n\t\t\t...entry,\n\t\t\tname: entry.display_name ?? entry.title ?? entry.name,\n\t\t},\n\t\tdefaults,\n\t\treference,\n\t);\n\treturn {\n\t\t...model,\n\t\treasoning: novitaArrayIncludes(entry.features, \"reasoning\"),\n\t\tsupportsTools: novitaArrayIncludes(entry.features, \"function-calling\"),\n\t\tinput: toInputCapabilities(entry.input_modalities),\n\t\tcost: {\n\t\t\tinput: toNovitaCostPerMillion(entry.input_token_price_per_m),\n\t\t\toutput: toNovitaCostPerMillion(entry.output_token_price_per_m),\n\t\t\tcacheRead: getNovitaCacheReadPricePerMillion(entry),\n\t\t\tcacheWrite: 0,\n\t\t},\n\t\tcontextWindow: toPositiveNumber(entry.context_size, model.contextWindow),\n\t\tmaxTokens: toPositiveNumber(entry.max_output_tokens, model.maxTokens),\n\t};\n}\n\n/** Builds Novita's public model-discovery manager. */\nexport function novitaModelManagerOptions(\n\tconfig?: NovitaModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.novita.ai/openai/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"novita\");\n\treturn {\n\t\tproviderId: \"novita\",\n\t\tdynamicModelsAuthoritative: true,\n\t\tfetchDynamicModels: async () =>\n\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"novita\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) => mapNovitaModel(entry, defaults, references.get(defaults.id)),\n\t\t\t\tfilterModel: (entry, model) => {\n\t\t\t\t\tconst active = typeof entry.status !== \"number\" || entry.status === 1;\n\t\t\t\t\treturn (\n\t\t\t\t\t\tactive &&\n\t\t\t\t\t\tisPublicNovitaModelId(model.id) &&\n\t\t\t\t\t\tnovitaArrayIncludes(entry.endpoints, \"chat/completions\") &&\n\t\t\t\t\t\ttoPositiveNumber(entry.max_output_tokens, 0) > 0\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 6. xAI\n// ---------------------------------------------------------------------------\n\nexport interface XaiModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"xai\", \"https://api.x.ai/v1\", config);\n}\n\nexport interface XaiOAuthModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\ninterface XAICuratedModel {\n\tid: string;\n\tcontextWindow: number;\n\tname?: string;\n\t/** Whether the model reasons natively. Defaults to true for Grok-4.x family. */\n\treasoning?: boolean;\n\t/**\n\t * Whether xAI accepts the `reasoning.effort` wire param for this model.\n\t * Default true. When false: the picker hides the effort dial (via\n\t * getSupportedEfforts in model-thinking.ts) AND the wire omits the param —\n\t * both derive from `isGrokReasoningEffortCapable` (identity/family.ts), the\n\t * single allowlist shared by this curated layer and the compat builder.\n\t */\n\tsupportsReasoningEffort?: boolean;\n\t/**\n\t * Input modalities this model accepts. Defaults to `[\"text\"]` when absent.\n\t * Vision-capable Grok models MUST list `\"image\"` here so the curated layer\n\t * overrides `fetchOpenAICompatibleModels`' default of `[\"text\"]` (which\n\t * otherwise strips image capability on every online refresh).\n\t */\n\tinput?: (\"text\" | \"image\")[];\n}\n\n// Source of truth for the xai-oauth chat picker. Top of list = headline.\n// Context windows from hermes-agent/agent/model_metadata.py:205-220\n// (\"Values sourced from models.dev (2026-04)\"). grok-build is xAI's\n// coding-fine-tuned chat model; 512K context per user spec (2026-05-17).\n//\n// supportsReasoningEffort=false entries reason natively but reject the wire\n// `reasoning.effort` param (api.x.ai returns HTTP 400). The corresponding\n// omit/include/history replay defaults live in catalog compat so every\n// OpenAI-family endpoint consumes the same constraint.\nexport const XAI_OAUTH_CURATED_MODELS: readonly XAICuratedModel[] = [\n\t{\n\t\tid: \"grok-build\",\n\t\tcontextWindow: 512_000,\n\t\tname: \"Grok Build\",\n\t\tsupportsReasoningEffort: false,\n\t\tinput: [\"text\", \"image\"],\n\t},\n\t{\n\t\tid: \"grok-build-0.1\",\n\t\tcontextWindow: 256_000,\n\t\tname: \"Grok Build 0.1\",\n\t\tsupportsReasoningEffort: false,\n\t\tinput: [\"text\", \"image\"],\n\t},\n\t{ id: \"grok-4.3\", contextWindow: 1_000_000, name: \"Grok 4.3\", input: [\"text\", \"image\"] },\n\t{ id: \"grok-4.5\", contextWindow: 500_000, name: \"Grok 4.5\", input: [\"text\", \"image\"] },\n\t// grok-4.20-multi-agent-0309 is text-only per the bundled catalog; omit `input` for the default.\n\t{ id: \"grok-4.20-multi-agent-0309\", contextWindow: 2_000_000, name: \"Grok 4.20 (Multi-Agent)\" },\n\t{\n\t\tid: \"grok-4.20-0309-reasoning\",\n\t\tcontextWindow: 2_000_000,\n\t\tname: \"Grok 4.20 (Reasoning)\",\n\t\tsupportsReasoningEffort: false,\n\t\tinput: [\"text\", \"image\"],\n\t},\n\t{\n\t\tid: \"grok-4.20-0309-non-reasoning\",\n\t\tcontextWindow: 2_000_000,\n\t\tname: \"Grok 4.20 (Non-Reasoning)\",\n\t\treasoning: false,\n\t\tinput: [\"text\", \"image\"],\n\t},\n\t// Cursor's \"Composer 2.5 Fast\" exposed via SuperGrok: non-reasoning,\n\t// text-only, 200K context (mirrors Cursor's composer-* catalog entries).\n\t// Off the Grok effort-capable allowlist; reasoning:false also hides the effort dial.\n\t{\n\t\tid: \"grok-composer-2.5-fast\",\n\t\tcontextWindow: 200_000,\n\t\tname: \"Grok Composer 2.5 Fast\",\n\t\treasoning: false,\n\t\tinput: [\"text\"],\n\t},\n] as const;\n\n// xAI /v1/models returns chat, image, voice, and STT entries. Tool surfaces\n// route through dedicated tools (generate_image, tts) with their own model\n// strings; the chat picker MUST exclude these prefixes or selecting them 400s.\nconst XAI_NON_CHAT_PREFIXES = [\"grok-imagine-\", \"grok-stt-\", \"grok-voice-\"] as const;\n\nfunction withXaiOAuthCompatDefaults(model: ModelSpec<\"openai-responses\">): ModelSpec<\"openai-responses\"> {\n\tconst compat = {\n\t\t...(model.compat ?? {}),\n\t\tincludeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? false,\n\t\tfilterReasoningHistory: model.compat?.filterReasoningHistory ?? true,\n\t\tsupportsImageDetailOriginal: model.compat?.supportsImageDetailOriginal ?? false,\n\t\tomitReasoningEffort: model.compat?.omitReasoningEffort ?? !isGrokReasoningEffortCapable(model.id),\n\t};\n\treturn { ...model, compat };\n}\n\n// Hermes-agent parity: only the `minimal -> low` clamp is applied (see\n// hermes-agent/agent/transports/codex.py:92 `_effort_clamp = {\"minimal\":\n// \"low\"}`). Hermes sends `xhigh` to xAI verbatim and we match that contract\n// — let xAI decide if the level is valid for the specific Grok model.\n// `resolveModelThinking` folds this into `model.thinking.effortMap`, downstream\n// of the omitReasoningEffort gate in pi-ai's stream.ts.\nconst XAI_REASONING_EFFORT_MAP = { minimal: \"low\" } as const;\n\n// xai-oauth's /v1/models exposes no per-request output limit on the OAuth\n// (Grok Build / SuperGrok) surface, so the curated catalog owns `maxTokens`\n// like it owns `contextWindow`: each entry mirrors its context window. The\n// openai-responses wire clamps the actual request to\n// min(requested, model.maxTokens, OPENAI_MAX_OUTPUT_TOKENS=64000), so this is\n// just \"no model-specific sub-cap below 64k\", not an unbounded output budget.\n\n// Single source of truth for curated → Model fan-in. Used by the static-seed\n// and the dynamic overlay/inject paths (applyXAIOAuthCuration) so curated\n// reasoning/effort flags survive an online refresh (xAI's /v1/models lacks\n// reasoning metadata and fetchOpenAICompatibleModels defaults reasoning to\n// false). Caller supplies a `base` Model (either a freshly synthesised seed\n// or a dynamic-fetched entry); the helper layers curated fields on top.\n// The `minimal -> low` effort clamp (XAI_REASONING_EFFORT_MAP) is always\n// merged in so dynamic-fetched models — which arrive without curated\n// compat keys — still get the clamp applyResponsesReasoningParams expects.\n// The effort-dial pair (`supportsReasoningEffort`/`omitReasoningEffort`) is\n// authoritative: a stale flag on `base` (previous snapshot or dynamic fetch)\n// must not outlive an allowlist change in identity/family.ts.\nfunction mergeCuratedIntoModel(\n\tbase: ModelSpec<\"openai-responses\">,\n\tcurated: XAICuratedModel,\n): ModelSpec<\"openai-responses\"> {\n\tconst effortCapable = curated.supportsReasoningEffort ?? isGrokReasoningEffortCapable(curated.id);\n\tconst compat = {\n\t\t...(base.compat ?? {}),\n\t\treasoningEffortMap: { ...XAI_REASONING_EFFORT_MAP, ...(base.compat?.reasoningEffortMap ?? {}) },\n\t\tincludeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? false,\n\t\tfilterReasoningHistory: base.compat?.filterReasoningHistory ?? true,\n\t\tsupportsImageDetailOriginal: base.compat?.supportsImageDetailOriginal ?? false,\n\t\tomitReasoningEffort: !effortCapable,\n\t\tsupportsReasoningEffort: effortCapable,\n\t};\n\treturn {\n\t\t...base,\n\t\tcontextWindow: curated.contextWindow,\n\t\tmaxTokens: curated.contextWindow,\n\t\tname: curated.name ?? base.name,\n\t\treasoning: curated.reasoning ?? true,\n\t\tinput: curated.input ?? base.input,\n\t\tcompat,\n\t};\n}\n\n/**\n * Overlay/inject curated xai-oauth metadata onto dynamic-fetch results so\n * a successful `online refresh` doesn't regress vision capability, context\n * window, reasoning flags, or the effort-dial allowlist.\n *\n * Three passes:\n * 1. Filter `XAI_NON_CHAT_PREFIXES` (picker pollution defense for tool\n * surfaces routed through dedicated tools — generate_image, tts).\n * 2. Overlay curated metadata onto dynamic-fetch matches. xAI's /v1/models\n * does not return context_window or reasoning metadata, so without\n * this overlay the runtime falls back to the bundled-reference default\n * (effectively 128k context) and `reasoning: false` (suppressing the\n * effort dial and stripping thinking metadata downstream).\n * 3. Inject curated entries missing from the dynamic fetch. Clones the\n * first surviving entry as a template so required Model fields (api,\n * provider, baseUrl, cost, etc.) inherit sane defaults. If `filtered`\n * is empty (offline / no auth) injection is skipped — the descriptor's\n * defaultModel covers the fallback.\n *\n * Order: curated models first in declaration order; then dynamic remainder\n * in original order.\n */\nfunction applyXAIOAuthCuration(dynamic: readonly ModelSpec<\"openai-responses\">[]): ModelSpec<\"openai-responses\">[] {\n\tconst filtered = dynamic.filter(e => !XAI_NON_CHAT_PREFIXES.some(p => e.id.startsWith(p)));\n\n\tconst byId = new Map<string, ModelSpec<\"openai-responses\">>(filtered.map(e => [e.id, e]));\n\tfor (const curated of XAI_OAUTH_CURATED_MODELS) {\n\t\tconst existing = byId.get(curated.id);\n\t\tif (existing) {\n\t\t\tbyId.set(curated.id, mergeCuratedIntoModel(existing, curated));\n\t\t}\n\t}\n\n\tconst template = filtered[0];\n\tif (template) {\n\t\tfor (const curated of XAI_OAUTH_CURATED_MODELS) {\n\t\t\tif (!byId.has(curated.id)) {\n\t\t\t\t// Reset id/name on the template before merging so the helper's\n\t\t\t\t// `curated.name ?? base.name` clause falls back to curated.id\n\t\t\t\t// (the inject contract), not to the unrelated template's label.\n\t\t\t\tconst base: ModelSpec<\"openai-responses\"> = { ...template, id: curated.id, name: curated.id };\n\t\t\t\tbyId.set(curated.id, mergeCuratedIntoModel(base, curated));\n\t\t\t}\n\t\t}\n\t}\n\n\tconst curatedIds = new Set(XAI_OAUTH_CURATED_MODELS.map(c => c.id));\n\tconst curatedFirst = XAI_OAUTH_CURATED_MODELS.map(c => byId.get(c.id)).filter(\n\t\t(e): e is ModelSpec<\"openai-responses\"> => e !== undefined,\n\t);\n\tconst rest = filtered.filter(e => !curatedIds.has(e.id)).map(withXaiOAuthCompatDefaults);\n\treturn [...curatedFirst, ...rest];\n}\n\n/**\n * Render `XAI_OAUTH_CURATED_MODELS` as full `ModelSpec<\"openai-responses\">` entries.\n *\n * Single source of truth for the curated to Model fan-in, consumed by both\n * - {@link xaiOAuthModelManagerOptions} (runtime static seed handed to the model\n * manager so the picker is populated on a fresh login), and\n * - \\`packages/catalog/scripts/generate-models.ts\\` (bundles the same entries into\n * `models.json`, so the synchronous `ModelRegistry.#loadModels()` boot path\n * sees `xai-oauth` without waiting for a refresh — fixes the boot-time\n * default-model reset when `modelRoles.default = \"xai-oauth/<id>\"`).\n *\n * `reasoning` defaults to `true` for the Grok-4.x family; the explicit\n * `grok-4.20-0309-non-reasoning` entry opts out via `XAICuratedModel.reasoning`.\n * `maxTokens` mirrors each model's `contextWindow` (the OAuth surface reports\n * no per-request output limit); the openai-responses wire still clamps the\n * actual request to OPENAI_MAX_OUTPUT_TOKENS. Mirrors\n * `hermes-agent/hermes_cli/models.py:_XAI_STATIC_FALLBACK`.\n */\nexport function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<\"openai-responses\">[] {\n\tconst resolvedBaseUrl = baseUrl ?? \"https://api.x.ai/v1\";\n\treturn XAI_OAUTH_CURATED_MODELS.map(curated => {\n\t\t// Synthesise a bare base then layer curated metadata via the same helper\n\t\t// the dynamic overlay/inject paths use. `name: curated.id` is a sentinel\n\t\t// the helper rewrites to `curated.name ?? base.name`, so curated.name\n\t\t// wins when set.\n\t\tconst base: ModelSpec<\"openai-responses\"> = {\n\t\t\tid: curated.id,\n\t\t\tname: curated.id,\n\t\t\tapi: \"openai-responses\",\n\t\t\tprovider: \"xai-oauth\",\n\t\t\tbaseUrl: resolvedBaseUrl,\n\t\t\treasoning: true,\n\t\t\tinput: [\"text\"],\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\tcontextWindow: curated.contextWindow,\n\t\t\tmaxTokens: curated.contextWindow,\n\t\t\tcompat: { reasoningEffortMap: XAI_REASONING_EFFORT_MAP },\n\t\t};\n\t\treturn mergeCuratedIntoModel(base, curated);\n\t});\n}\n\nexport function xaiOAuthModelManagerOptions(\n\tconfig?: XaiOAuthModelManagerConfig,\n): ModelManagerOptions<\"openai-responses\"> {\n\tconst defaultBaseUrl = \"https://api.x.ai/v1\";\n\tconst resolvedBaseUrl = config?.baseUrl ?? defaultBaseUrl;\n\tconst base = createSimpleOpenAIResponsesOptions(\n\t\t\"xai-oauth\" as Parameters<typeof getBundledModels>[0],\n\t\tdefaultBaseUrl,\n\t\tconfig,\n\t);\n\t// Static seed handed to the runtime model manager so the picker populates on\n\t// a fresh login even before `fetchDynamicModels` fires (it is gated on\n\t// `config.apiKey` at construction time, and OAuth tokens resolve later via\n\t// AuthStorage). \\`generate-models.ts\\` calls the same builder so \\`models.json\\`\n\t// carries these entries too — making the synchronous `#loadModels()` boot\n\t// path honor `modelRoles.default = \"xai-oauth/<id>\"` without `await refresh()`.\n\tconst staticModels = buildXaiOAuthStaticSeed(resolvedBaseUrl);\n\tif (!base.fetchDynamicModels) {\n\t\treturn { ...base, staticModels };\n\t}\n\t// Wrap fetchDynamicModels so an `online refresh` against xAI's /v1/models\n\t// runs through applyXAIOAuthCuration — preserves curated context windows,\n\t// vision modality, reasoning flags, and filters tool-only model ids\n\t// (grok-imagine-*, grok-stt-*, grok-voice-*) from the chat picker.\n\tconst inner = base.fetchDynamicModels;\n\treturn {\n\t\t...base,\n\t\tstaticModels,\n\t\tfetchDynamicModels: async () => {\n\t\t\tconst dynamic = await inner();\n\t\t\treturn dynamic == null ? dynamic : applyXAIOAuthCuration(dynamic);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 6.4 AIML API\n// ---------------------------------------------------------------------------\n\nconst AIML_API_NON_CHAT_MODEL_ID_PATTERN =\n\t/(?:^|[/:._-])(?:audio|embed|embedding|embeddings|i2i|i2v|image|speech|t2i|t2v|tts|video)(?:$|[/:._-])/i;\n\nconst AIML_API_NON_CHAT_MODEL_ID_SUBSTRINGS = [\"dall-e\", \"dalle\", \"flux\", \"imagen\", \"sora\", \"veo\", \"whisper\"] as const;\n\nexport function isLikelyAimlApiChatModelId(id: string): boolean {\n\tconst normalized = id.trim().toLowerCase();\n\tif (!normalized) return false;\n\treturn (\n\t\t!AIML_API_NON_CHAT_MODEL_ID_PATTERN.test(normalized) &&\n\t\t!AIML_API_NON_CHAT_MODEL_ID_SUBSTRINGS.some(token => normalized.includes(token))\n\t);\n}\n\nexport interface AimlApiModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function aimlApiModelManagerOptions(\n\tconfig?: AimlApiModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.aimlapi.com/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"aimlapi\");\n\treturn {\n\t\tproviderId: \"aimlapi\",\n\t\tdynamicModelsAuthoritative: true,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"aimlapi\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tfilterModel: (_entry, model) => isLikelyAimlApiChatModelId(model.id),\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\treturn mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 6.5 DeepSeek\n// ---------------------------------------------------------------------------\n\nexport interface DeepSeekModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function deepseekModelManagerOptions(\n\tconfig?: DeepSeekModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"deepseek\", \"https://api.deepseek.com\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 6.6 SiliconFlow\n// ---------------------------------------------------------------------------\n\nexport interface SiliconFlowModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\n/**\n * SiliconFlow's `/v1/models` lists every served model — including embeddings,\n * rerankers, image, audio, and video generators that cannot serve chat\n * completions — and carries no per-model type field, so non-chat entries are\n * dropped by id to keep the picker usable.\n */\nconst SILICONFLOW_NON_CHAT_MODEL_TOKENS = [\n\t\"embedding\",\n\t\"reranker\",\n\t\"bge-\",\n\t\"bce-\",\n\t\"stable-diffusion\",\n\t\"image\",\n\t\"flux\",\n\t\"kolors\",\n\t\"sensevoice\",\n\t\"cosyvoice\",\n\t\"fish-speech\",\n\t\"indextts\",\n\t\"sovits\",\n\t\"whisper\",\n\t\"hunyuanvideo\",\n\t\"wan2\",\n\t\"ltx-video\",\n\t\"speech\",\n\t\"moderator\",\n\t\"tts\",\n] as const;\n\nexport function isLikelySiliconFlowChatModelId(id: string): boolean {\n\tconst normalized = id.trim().toLowerCase();\n\tif (!normalized) {\n\t\treturn false;\n\t}\n\treturn !SILICONFLOW_NON_CHAT_MODEL_TOKENS.some(token => normalized.includes(token));\n}\n\n/**\n * models.dev mappings consulted ONLY as a runtime metadata reference during\n * dynamic discovery. They are deliberately absent from\n * `MODELS_DEV_PROVIDER_DESCRIPTORS` so `generate-models.ts` never bundles\n * SiliconFlow models — the live endpoint decides which models exist, while\n * these entries hydrate the pricing, limits, and reasoning metadata that the\n * endpoint's bare `{id}` rows do not carry. No filter: the join against live\n * discovered ids already restricts hydration to chat models.\n */\nconst SILICONFLOW_MODELS_DEV_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] = [\n\topenAiCompletionsDescriptor(\"siliconflow\", \"siliconflow\", \"https://api.siliconflow.com/v1\", {\n\t\tfilterModel: () => true,\n\t}),\n\topenAiCompletionsDescriptor(\"siliconflow-cn\", \"siliconflow-cn\", \"https://api.siliconflow.cn/v1\", {\n\t\tfilterModel: () => true,\n\t}),\n];\n\nconst SILICONFLOW_MODELS_DEV_REFERENCE_TIMEOUT_MS = 5_000;\n\nasync function loadSiliconFlowModelsDevReferences(\n\tproviderId: \"siliconflow\" | \"siliconflow-cn\",\n\tfetchImpl?: FetchImpl,\n): Promise<Map<string, ModelSpec<\"openai-completions\">>> {\n\tconst descriptor = SILICONFLOW_MODELS_DEV_DESCRIPTORS.find(d => d.providerId === providerId);\n\tif (!descriptor) {\n\t\treturn new Map();\n\t}\n\ttry {\n\t\t// Bounded: this enrichment is optional, so a stalled models.dev must not\n\t\t// hold back the authoritative endpoint request that runs after it.\n\t\tconst payload = await withCatalogDiscoveryTimeout(SILICONFLOW_MODELS_DEV_REFERENCE_TIMEOUT_MS, signal =>\n\t\t\tfetchWellKnownModels(fetchImpl, signal),\n\t\t);\n\t\treturn createModelsDevReferenceMap<\"openai-completions\">(\n\t\t\tmapModelsDevToModels(payload as Record<string, unknown>, [descriptor]),\n\t\t);\n\t} catch {\n\t\treturn new Map();\n\t}\n}\n\nfunction createSiliconFlowModelManagerOptions(\n\tproviderId: \"siliconflow\" | \"siliconflow-cn\",\n\tdefaultBaseUrl: string,\n\tconfig?: SiliconFlowModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? defaultBaseUrl;\n\treturn {\n\t\tproviderId,\n\t\tdynamicModelsAuthoritative: true,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: async () => {\n\t\t\t\tconst modelsDevReferences = await loadSiliconFlowModelsDevReferences(providerId, config?.fetch);\n\t\t\t\t// Resolved here, not at options construction: walking the bundled\n\t\t\t\t// reference index is only worth paying for when dynamic discovery\n\t\t\t\t// actually runs, keeping the ModelManager cache fast path cheap.\n\t\t\t\tconst canonicalReferences = getBundledModelReferenceIndex();\n\t\t\t\treturn fetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: providerId,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tfilterModel: (_entry, model) => isLikelySiliconFlowChatModelId(model.id),\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst modelsDevReference = modelsDevReferences.get(defaults.id);\n\t\t\t\t\t\tif (modelsDevReference) {\n\t\t\t\t\t\t\treturn mapWithBundledReference(entry, defaults, modelsDevReference);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// ids missing from models.dev (new launches) still recover intrinsic\n\t\t\t\t\t\t// capabilities and canonical limits from any bundled upstream/reseller\n\t\t\t\t\t\t// entry — but never its pricing, which is provider-specific.\n\t\t\t\t\t\tconst canonical = resolveModelReference(defaults.id, canonicalReferences) as\n\t\t\t\t\t\t\t| ModelSpec<\"openai-completions\">\n\t\t\t\t\t\t\t| undefined;\n\t\t\t\t\t\tif (!canonical) {\n\t\t\t\t\t\t\treturn defaults;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst contextWindow = canonical.contextWindow ?? defaults.contextWindow;\n\t\t\t\t\t\tconst maxTokens =\n\t\t\t\t\t\t\tcanonical.maxTokens != null && contextWindow != null\n\t\t\t\t\t\t\t\t? Math.min(canonical.maxTokens, contextWindow)\n\t\t\t\t\t\t\t\t: (canonical.maxTokens ?? defaults.maxTokens);\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\t\tname: toModelName(entry.name, canonical.name ?? defaults.name),\n\t\t\t\t\t\t\treasoning: canonical.reasoning,\n\t\t\t\t\t\t\tinput: canonical.input,\n\t\t\t\t\t\t\tcontextWindow,\n\t\t\t\t\t\t\tmaxTokens,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t});\n\t\t\t},\n\t\t}),\n\t};\n}\n\nexport function siliconflowModelManagerOptions(\n\tconfig?: SiliconFlowModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSiliconFlowModelManagerOptions(\"siliconflow\", \"https://api.siliconflow.com/v1\", config);\n}\n\nexport function siliconflowCnModelManagerOptions(\n\tconfig?: SiliconFlowModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSiliconFlowModelManagerOptions(\"siliconflow-cn\", \"https://api.siliconflow.cn/v1\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 6.7 Zhipu Coding Plan\n// ---------------------------------------------------------------------------\n\nexport interface ZhipuCodingPlanModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function zhipuCodingPlanModelManagerOptions(\n\tconfig?: ZhipuCodingPlanModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://open.bigmodel.cn/api/coding/paas/v4\";\n\treturn {\n\t\tproviderId: \"zhipu-coding-plan\",\n\t\tdynamicModelsAuthoritative: true,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"zhipu-coding-plan\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (\n\t\t\t\t\t\t_entry: OpenAICompatibleModelRecord,\n\t\t\t\t\t\tdefaults: ModelSpec<\"openai-completions\">,\n\t\t\t\t\t\t_context: OpenAICompatibleModelMapperContext<\"openai-completions\">,\n\t\t\t\t\t): ModelSpec<\"openai-completions\"> => {\n\t\t\t\t\t\tconst id = defaults.id;\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\t\treasoning: isReasoningGlmModelId(id) || id.includes(\"thinking\"),\n\t\t\t\t\t\t\tinput: isGlmVisionModelId(id) ? ([\"text\", \"image\"] as const) : [\"text\"],\n\t\t\t\t\t\t\tcompat: {\n\t\t\t\t\t\t\t\tthinkingFormat: \"zai\",\n\t\t\t\t\t\t\t\treasoningContentField: \"reasoning_content\",\n\t\t\t\t\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 7.5 Fireworks\n// ---------------------------------------------------------------------------\n\n/**\n * Fireworks-published cap for the Kimi K2 family. Fireworks' `/v1/models`\n * envelope generically reports `max_completion_tokens: 65536` for every Kimi\n * deployment, but Kimi K2 (instruct / thinking / turbo) on Fireworks is\n * documented to ship long reasoning traces that should be bounded — capping\n * at 32,768 prevents handing callers a budget the router cannot honor.\n * See https://github.com/can1357/oh-my-pi/issues/1849.\n */\nexport const FIREWORKS_KIMI_MAX_TOKENS = 32_768;\n\n/**\n * Fireworks' output ceiling for Kimi K2.7-Code specifically. Its `/v1/models`\n * generic `max_completion_tokens` is 65,536 and Fireworks serves it in full —\n * verified with a single completion emitting 58,971 output tokens and\n * `max_tokens: 200000` accepted without error. Unlike the older K2.5/K2.6\n * family (see {@link FIREWORKS_KIMI_MAX_TOKENS}), K2.7-Code is not clamped to\n * 32,768; that ceiling only truncated it.\n */\nexport const FIREWORKS_KIMI_K27_CODE_MAX_TOKENS = 65_536;\n\n/**\n * Returns true for the Kimi K2.5 / K2.6 family served by Fireworks-backed\n * providers (`fireworks` direct, `firepass` router) that share the 32,768\n * `maxTokens` ceiling. Matches both the public catalog id (`kimi-k2.5`,\n * `kimi-k2.6`, `kimi-k2.6-turbo`) and the canonical Fireworks wire id\n * (`accounts/fireworks/{models,routers}/kimi-k2…`).\n *\n * K2.7-Code (incl. `-fast` / `-highspeed`) is deliberately excluded: unlike the\n * earlier K2 family it serves its full context on Fireworks — verified with a\n * single completion emitting 58,971 output tokens and `max_tokens: 200000`\n * accepted without error — so the 32,768 cap would only truncate it. It inherits\n * Fireworks' reported `max_completion_tokens` (65,536) instead.\n */\nexport function isFireworksKimiK2ModelId(modelId: string): boolean {\n\tconst trimmed = modelId.toLowerCase();\n\tif (/kimi[-._]?k2(?:[._-]?|p)7[-._]?code/.test(trimmed)) return false;\n\tif (trimmed.startsWith(\"kimi-k2\")) return true;\n\treturn /\\/kimi-k2(?:p\\d+)?(?:[._-]|$)/.test(trimmed);\n}\n\n/**\n * Clamp the Kimi K2 family's `maxTokens` to {@link FIREWORKS_KIMI_MAX_TOKENS}\n * on Fireworks-backed providers, leaving every other model untouched.\n */\nexport function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number;\nexport function clampFireworksKimiMaxTokens(modelId: string, candidate: number | null): number | null;\nexport function clampFireworksKimiMaxTokens(modelId: string, candidate: number | null): number | null {\n\tif (candidate === null) return null;\n\treturn isFireworksKimiK2ModelId(modelId) ? Math.min(candidate, FIREWORKS_KIMI_MAX_TOKENS) : candidate;\n}\n\n/**\n * Kimi K2.7 Code's documented recommended output budget. Some provider\n * discovery rows report the context-sized `max_completion_tokens` instead.\n */\nexport const KIMI_K27_CODE_RECOMMENDED_MAX_TOKENS = 32_768;\n\nexport function isKimiK27CodeModelId(modelId: string): boolean {\n\treturn /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i.test(modelId);\n}\n\nexport function clampKimiK27CodeMaxTokens(modelId: string, candidate: number): number;\nexport function clampKimiK27CodeMaxTokens(modelId: string, candidate: number | null): number | null;\nexport function clampKimiK27CodeMaxTokens(modelId: string, candidate: number | null): number | null {\n\tif (candidate === null) return null;\n\treturn isKimiK27CodeModelId(modelId) ? Math.min(candidate, KIMI_K27_CODE_RECOMMENDED_MAX_TOKENS) : candidate;\n}\n\n/**\n * Fireworks Fast variants we surface. Each inherits the base model's\n * limits/modalities/thinking and overrides only the cost with the Standard-column\n * Fast prices from the Serverless pricing table; `cacheWrite` stays 0 (Fireworks\n * bills no cache-write). Derived from the bundled base entries so metadata stays\n * in lockstep, and the runtime auto-falls back to the base id on a failed fast\n * request. See https://docs.fireworks.ai/serverless/pricing.\n */\nconst FIREWORKS_FAST_VARIANT_SPECS: ReadonlyArray<{\n\tbase: string;\n\tname: string;\n\tcost: { input: number; output: number; cacheRead: number };\n}> = [\n\t{ base: \"kimi-k2.7-code\", name: \"Kimi K2.7 Code Fast\", cost: { input: 1.9, output: 8, cacheRead: 0.38 } },\n\t{ base: \"kimi-k2.6\", name: \"Kimi K2.6 Fast\", cost: { input: 2, output: 8, cacheRead: 0.3 } },\n\t{ base: \"glm-5.1\", name: \"GLM-5.1 Fast\", cost: { input: 2.8, output: 8.8, cacheRead: 0.52 } },\n];\n\n/**\n * Build the Fireworks Fast seed by projecting each base bundled spec into a\n * `<id>-fast` variant. Pushed into the generated catalog (Fast routers never\n * appear in the serverless control-plane list, so discovery cannot surface\n * them) and deduped behind any identical previous-snapshot entry.\n */\nexport function buildFireworksFastSeed(): ModelSpec<\"openai-completions\">[] {\n\tconst bundled = createBundledReferenceMap<\"openai-completions\">(\"fireworks\");\n\tconst seeds: ModelSpec<\"openai-completions\">[] = [];\n\tfor (const variant of FIREWORKS_FAST_VARIANT_SPECS) {\n\t\tconst base = bundled.get(variant.base);\n\t\tif (!base) continue;\n\t\tseeds.push({\n\t\t\t...base,\n\t\t\tid: `${variant.base}${FIREWORKS_FAST_SUFFIX}`,\n\t\t\tname: variant.name,\n\t\t\tcost: {\n\t\t\t\tinput: variant.cost.input,\n\t\t\t\toutput: variant.cost.output,\n\t\t\t\tcacheRead: variant.cost.cacheRead,\n\t\t\t\tcacheWrite: 0,\n\t\t\t},\n\t\t});\n\t}\n\treturn seeds;\n}\n\n/**\n * Fireworks DeepSeek V4 accepts effort via `reasoning_effort` but rejects the\n * DeepSeek-native binary `thinking` toggle when both are present.\n */\nexport function stripFireworksDeepSeekThinkingToggle(\n\tmodel: ModelSpec<\"openai-completions\">,\n\tpublicModelId: string,\n): ModelSpec<\"openai-completions\"> {\n\tif (!publicModelId.startsWith(\"deepseek-v4\")) return model;\n\tconst compat = model.compat;\n\tif (!compat?.extraBody || !(\"thinking\" in compat.extraBody)) return model;\n\n\tconst extraBody = { ...compat.extraBody };\n\tdelete extraBody.thinking;\n\tif (Object.keys(extraBody).length > 0) {\n\t\treturn { ...model, compat: { ...compat, extraBody } };\n\t}\n\n\tconst nextCompat = { ...compat };\n\tdelete nextCompat.extraBody;\n\treturn { ...model, compat: nextCompat };\n}\n\nexport interface FireworksModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nconst FIREWORKS_CONTROL_PLANE_ACCOUNT = \"fireworks\";\nconst FIREWORKS_SERVERLESS_FILTER = \"supports_serverless=true\";\nconst FIREWORKS_CONTROL_PLANE_PAGE_SIZE = 200;\nconst FIREWORKS_CONTROL_PLANE_MAX_PAGES = 25;\n\n/**\n * One record from the Fireworks control-plane catalog\n * (`GET /v1/accounts/{account}/models`). This is distinct from the\n * OpenAI-compatible `/v1/models` inference envelope: the control plane\n * enumerates the full serverless catalog with camelCase capability metadata,\n * including on-demand models (e.g. `kimi-k2p7-code`) that never surface in\n * `/v1/models`. Discovering here is what keeps new serverless models appearing\n * without catalog edits — see the Fireworks docs `List Models` API.\n */\ninterface FireworksControlPlaneModel {\n\t/** Resource name, e.g. `accounts/fireworks/models/kimi-k2p7-code`. */\n\tname?: unknown;\n\tdisplayName?: unknown;\n\tcontextLength?: unknown;\n\tsupportsImageInput?: unknown;\n\tsupportsTools?: unknown;\n\tsupportsServerless?: unknown;\n\tstate?: unknown;\n}\n\n/**\n * Derive the control-plane list endpoint from the inference base URL. The\n * inference API lives under `/inference/v1` while the control plane is\n * `/v1/accounts/<account>/models` on the same origin, so we route off origin.\n * Returns null for unparseable overrides (custom gateways) so discovery falls\n * back to the cached/bundled catalog.\n */\nfunction toFireworksControlPlaneModelsUrl(baseUrl: string, account: string): string | null {\n\ttry {\n\t\treturn `${new URL(baseUrl).origin}/v1/accounts/${account}/models`;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction mapFireworksControlPlaneModel(\n\trecord: FireworksControlPlaneModel,\n\tpublicModelId: string,\n\treference: ModelSpec<\"openai-completions\"> | undefined,\n\tbaseUrl: string,\n): ModelSpec<\"openai-completions\"> {\n\tconst name = toModelName(record.displayName, reference?.name ?? publicModelId);\n\tconst supportsImage = toBoolean(record.supportsImageInput) === true;\n\tconst supportsTools = toBoolean(record.supportsTools);\n\tconst contextWindow = toPositiveNumber(record.contextLength, reference?.contextWindow ?? null);\n\t// The control plane reports no max-output budget. Default K2.7-Code to its\n\t// verified 65,536 ceiling, the older K2.5/K2.6 family to the clamped 32,768,\n\t// everyone else to the discovery fallback, then clamp.\n\tconst fallbackMaxTokens = isKimiK27CodeModelId(publicModelId)\n\t\t? FIREWORKS_KIMI_K27_CODE_MAX_TOKENS\n\t\t: isFireworksKimiK2ModelId(publicModelId)\n\t\t\t? FIREWORKS_KIMI_MAX_TOKENS\n\t\t\t: null;\n\tconst maxTokens = clampFireworksKimiMaxTokens(publicModelId, reference?.maxTokens ?? fallbackMaxTokens);\n\tconst base: ModelSpec<\"openai-completions\"> = reference ?? {\n\t\tid: publicModelId,\n\t\tname,\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"fireworks\",\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow,\n\t\tmaxTokens,\n\t};\n\tconst model: ModelSpec<\"openai-completions\"> = {\n\t\t...base,\n\t\tid: publicModelId,\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"fireworks\",\n\t\tbaseUrl,\n\t\tname,\n\t\t// The control plane exposes capability flags but no reasoning bit. Every\n\t\t// serverless chat LLM Fireworks ships reasons, and `buildModel` derives\n\t\t// the Fireworks effort map from the id at build time — so default\n\t\t// unbundled models to reasoning while bundled references keep their value.\n\t\treasoning: reference?.reasoning ?? true,\n\t\tinput: supportsImage ? [\"text\", \"image\"] : (reference?.input ?? [\"text\"]),\n\t\tcontextWindow,\n\t\tmaxTokens,\n\t\t...(supportsTools === false ? { supportsTools: false } : {}),\n\t};\n\treturn stripFireworksDeepSeekThinkingToggle(model, publicModelId);\n}\n\n/**\n * Discover Fireworks serverless models via the control-plane `List Models`\n * API (`supports_serverless=true`), paginating the full catalog. Returns null\n * on any transport/protocol failure so the model manager keeps the cached or\n * bundled catalog rather than caching a truncated list as authoritative.\n */\nasync function fetchFireworksServerlessModels(options: {\n\tbaseUrl: string;\n\tapiKey: string;\n\tresolveReference: (publicModelId: string) => ModelSpec<\"openai-completions\"> | undefined;\n\tfetch?: FetchImpl;\n}): Promise<ModelSpec<\"openai-completions\">[] | null> {\n\tconst listUrl = toFireworksControlPlaneModelsUrl(options.baseUrl, FIREWORKS_CONTROL_PLANE_ACCOUNT);\n\tif (!listUrl) return null;\n\tconst fetchImpl = discoveryFetch(options.fetch);\n\tconst collected = new Map<string, ModelSpec<\"openai-completions\">>();\n\tlet pageToken = \"\";\n\tfor (let page = 0; page < FIREWORKS_CONTROL_PLANE_MAX_PAGES; page++) {\n\t\tconst url = new URL(listUrl);\n\t\turl.searchParams.set(\"filter\", FIREWORKS_SERVERLESS_FILTER);\n\t\turl.searchParams.set(\"pageSize\", String(FIREWORKS_CONTROL_PLANE_PAGE_SIZE));\n\t\tif (pageToken) url.searchParams.set(\"pageToken\", pageToken);\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await fetchImpl(url.toString(), {\n\t\t\t\tmethod: \"GET\",\n\t\t\t\theaders: { Accept: \"application/json\", Authorization: `Bearer ${options.apiKey}` },\n\t\t\t});\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t\tif (!response.ok) return null;\n\t\tlet payload: unknown;\n\t\ttry {\n\t\t\tpayload = await response.json();\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t\tif (!isRecord(payload)) return null;\n\t\tconst models = Array.isArray(payload.models) ? payload.models : [];\n\t\tfor (const entry of models) {\n\t\t\tif (!isRecord(entry)) continue;\n\t\t\tconst record = entry as FireworksControlPlaneModel;\n\t\t\tif (toBoolean(record.supportsServerless) !== true) continue;\n\t\t\tif (typeof record.state === \"string\" && record.state !== \"READY\") continue;\n\t\t\tconst wireName = typeof record.name === \"string\" ? record.name : \"\";\n\t\t\tif (!wireName) continue;\n\t\t\tconst publicModelId = toFireworksPublicModelId(wireName);\n\t\t\tif (!publicModelId) continue;\n\t\t\tcollected.set(\n\t\t\t\tpublicModelId,\n\t\t\t\tmapFireworksControlPlaneModel(\n\t\t\t\t\trecord,\n\t\t\t\t\tpublicModelId,\n\t\t\t\t\toptions.resolveReference(publicModelId),\n\t\t\t\t\toptions.baseUrl,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tconst next = typeof payload.nextPageToken === \"string\" ? payload.nextPageToken : \"\";\n\t\tif (!next) break;\n\t\tpageToken = next;\n\t}\n\treturn Array.from(collected.values());\n}\n\nfunction createModelsDevReferenceMap<TApi extends Api>(\n\tmodels: readonly ModelSpec<Api>[],\n): Map<string, ModelSpec<TApi>> {\n\tconst references = new Map<string, ModelSpec<TApi>>();\n\tfor (const model of models) {\n\t\tconst candidate = model as ModelSpec<TApi>;\n\t\tconst existing = references.get(candidate.id);\n\t\tif (!existing) {\n\t\t\treferences.set(candidate.id, candidate);\n\t\t\tcontinue;\n\t\t}\n\t\tif ((candidate.contextWindow ?? 0) > (existing.contextWindow ?? 0)) {\n\t\t\treferences.set(candidate.id, candidate);\n\t\t\tcontinue;\n\t\t}\n\t\tif (\n\t\t\tcandidate.contextWindow === existing.contextWindow &&\n\t\t\t(candidate.maxTokens ?? 0) > (existing.maxTokens ?? 0)\n\t\t) {\n\t\t\treferences.set(candidate.id, candidate);\n\t\t}\n\t}\n\treturn references;\n}\n\nasync function loadModelsDevReferences<TApi extends Api>(fetchImpl?: FetchImpl): Promise<Map<string, ModelSpec<TApi>>> {\n\ttry {\n\t\tconst payload = await fetchWellKnownModels(fetchImpl);\n\t\treturn createModelsDevReferenceMap<TApi>(\n\t\t\tmapModelsDevToModels(payload as Record<string, unknown>, MODELS_DEV_PROVIDER_DESCRIPTORS),\n\t\t);\n\t} catch {\n\t\treturn new Map<string, ModelSpec<TApi>>();\n\t}\n}\nexport function fireworksModelManagerOptions(\n\tconfig?: FireworksModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.fireworks.ai/inference/v1\";\n\tconst bundledReferences = createReferenceResolver(() =>\n\t\tcreateBundledReferenceMap<\"openai-completions\">(\"fireworks\"),\n\t);\n\treturn {\n\t\tproviderId: \"fireworks\",\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: async () => {\n\t\t\t\tconst modelsDevReferences = await loadModelsDevReferences<\"openai-completions\">(config?.fetch);\n\t\t\t\treturn fetchFireworksServerlessModels({\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tresolveReference: publicModelId =>\n\t\t\t\t\t\tmodelsDevReferences.get(publicModelId) ?? bundledReferences(publicModelId),\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t});\n\t\t\t},\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 7.6 Fire Pass (Fireworks Kimi K2.6 Turbo subscription)\n// ---------------------------------------------------------------------------\n\nexport interface FirepassModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\n/**\n * Fire Pass is a Fireworks subscription product that exposes a single router\n * model (Kimi K2.6 Turbo) under `accounts/fireworks/routers/kimi-k2p6-turbo`.\n * The dedicated `fpk_…` keys do not authorize `/v1/models`, so this manager\n * never performs dynamic discovery — the bundled catalog entry is canonical.\n * See https://docs.fireworks.ai/firepass.\n */\nexport function firepassModelManagerOptions(\n\t_config?: FirepassModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn {\n\t\tproviderId: \"firepass\",\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 7.7 Wafer Serverless\n// ---------------------------------------------------------------------------\n\nexport interface WaferModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nconst WAFER_DEFAULT_BASE_URL = \"https://pass.wafer.ai/v1\";\nconst WAFER_MAX_TOKENS_CAP = 65536;\n\n/**\n * Mapper for Wafer Serverless `/v1/models` records.\n *\n * Wafer wraps each entry with a `wafer` envelope describing capabilities and\n * pricing. The mapper folds that metadata into the canonical\n * `ModelSpec<\"openai-completions\">` shape and applies upstream-specific thinking\n * compat when the entry advertises reasoning support. Wafer pricing is exposed\n * through internal wholesale units; the public Serverless rate equals\n * `cents × 125 / 10000`.\n */\ninterface WaferRecord {\n\tcontext_length?: unknown;\n\ttier?: unknown;\n\tprovider?: unknown;\n\tcapabilities?: { vision?: unknown; reasoning?: unknown; tools?: unknown };\n\tpricing?: {\n\t\tinput_cents_per_million?: unknown;\n\t\toutput_cents_per_million?: unknown;\n\t\tcache_read_cents_per_million?: unknown;\n\t};\n\tdisplay_name?: unknown;\n}\n\nfunction readWaferRecord(entry: OpenAICompatibleModelRecord): WaferRecord | undefined {\n\tconst raw = (entry as { wafer?: unknown }).wafer;\n\treturn raw && typeof raw === \"object\" ? (raw as WaferRecord) : undefined;\n}\n\ntype WaferThinkingFormat = \"zai\" | \"qwen\";\n\nexport function resolveWaferServerlessThinkingFormat(\n\tmodelId: string,\n\tupstreamProvider: unknown,\n): WaferThinkingFormat | undefined {\n\tconst upstream = typeof upstreamProvider === \"string\" ? upstreamProvider.trim().toLowerCase() : \"\";\n\tif (upstream) {\n\t\tif (\n\t\t\tupstream === \"zai\" ||\n\t\t\tupstream === \"z.ai\" ||\n\t\t\tupstream === \"z-ai\" ||\n\t\t\tupstream.includes(\"zhipu\") ||\n\t\t\tupstream.includes(\"moonshot\") ||\n\t\t\tupstream.includes(\"kimi\")\n\t\t) {\n\t\t\treturn \"zai\";\n\t\t}\n\t\tif (upstream.includes(\"qwen\") || upstream.includes(\"alibaba\") || upstream.includes(\"dashscope\")) {\n\t\t\treturn \"qwen\";\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Older Wafer snapshots (and some endpoint responses) do not carry the\n\t// upstream-provider hint. Only GLM/Kimi need a sparse override: qwen and\n\t// deepseek IDs are resolved safely by `buildOpenAICompat` from the model id.\n\treturn isReasoningGlmModelId(modelId.toLowerCase()) || isKimiModelId(modelId) ? \"zai\" : undefined;\n}\n\nfunction mapWaferModel(\n\tproviderId: \"wafer-serverless\",\n\tbaseUrl: string,\n\tentry: OpenAICompatibleModelRecord,\n\tdefaults: ModelSpec<\"openai-completions\">,\n): ModelSpec<\"openai-completions\"> {\n\tconst wafer = readWaferRecord(entry);\n\tconst capabilities = wafer?.capabilities ?? {};\n\tconst reasoning = capabilities.reasoning === true;\n\tconst vision = capabilities.vision === true;\n\tconst supportsTools = toBoolean(capabilities.tools) === false ? false : undefined;\n\tconst contextWindow = toPositiveNumber(\n\t\twafer?.context_length,\n\t\ttoPositiveNumber((entry as { max_model_len?: unknown }).max_model_len, defaults.contextWindow),\n\t);\n\tconst maxTokens = contextWindow !== null ? Math.min(contextWindow, WAFER_MAX_TOKENS_CAP) : null;\n\tconst pricing = wafer?.pricing ?? {};\n\tconst cost = {\n\t\tinput: (toPositiveNumber(pricing.input_cents_per_million, 0) * 125) / 10000,\n\t\toutput: (toPositiveNumber(pricing.output_cents_per_million, 0) * 125) / 10000,\n\t\tcacheRead: (toPositiveNumber(pricing.cache_read_cents_per_million, 0) * 125) / 10000,\n\t\tcacheWrite: 0,\n\t};\n\tconst name = toModelName(wafer?.display_name, defaults.name);\n\tconst base: ModelSpec<\"openai-completions\"> = {\n\t\t...defaults,\n\t\tid: defaults.id,\n\t\tname,\n\t\tapi: \"openai-completions\",\n\t\tprovider: providerId,\n\t\tbaseUrl,\n\t\treasoning,\n\t\tinput: vision ? ([\"text\", \"image\"] as const) : [\"text\"],\n\t\tcost,\n\t\tcontextWindow,\n\t\tmaxTokens,\n\t\t...(supportsTools === false ? { supportsTools } : {}),\n\t};\n\tif (reasoning) {\n\t\t// Wafer's `wafer.provider` envelope tells us which upstream backend serves\n\t\t// the model. Each upstream accepts a different thinking-control parameter\n\t\t// on the wire — Wafer passes the body through, so we must mirror the\n\t\t// upstream's native shape:\n\t\t// - zai (GLM) and moonshotai (Kimi) → `thinking: { type: \"enabled\" | \"disabled\" }`\n\t\t// - qwen (Alibaba) → top-level `enable_thinking: boolean`\n\t\t// - deepseek → `reasoning_effort` (DeepSeek effort map; the model always\n\t\t// reasons when invoked, replay of `reasoning_content` is required on\n\t\t// tool-call turns — both handled by `detectOpenAICompat` from the id).\n\t\t// Unknown upstreams stay unset; missing upstreams fall back only for\n\t\t// model families that cannot be inferred safely from Wafer's host.\n\t\tconst thinkingFormat = resolveWaferServerlessThinkingFormat(defaults.id, wafer?.provider);\n\t\treturn {\n\t\t\t...base,\n\t\t\tcompat: {\n\t\t\t\t...(thinkingFormat ? { thinkingFormat } : {}),\n\t\t\t\treasoningContentField: \"reasoning_content\",\n\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t},\n\t\t};\n\t}\n\treturn {\n\t\t...base,\n\t\tcompat: { supportsDeveloperRole: false },\n\t};\n}\n\nexport function waferServerlessModelManagerOptions(\n\tconfig?: WaferModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? WAFER_DEFAULT_BASE_URL;\n\tconst providerId = \"wafer-serverless\" as const;\n\treturn {\n\t\tproviderId,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: providerId,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => mapWaferModel(providerId, baseUrl, entry, defaults),\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 7. Mistral\n// ---------------------------------------------------------------------------\n\nexport interface MistralModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function mistralModelManagerOptions(\n\tconfig?: MistralModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"mistral\", \"https://api.mistral.ai/v1\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 8. OpenCode\n// ---------------------------------------------------------------------------\n\nexport interface OpenCodeModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nfunction normalizeOpenCodeBasePath(baseUrl: string | undefined, fallbackBasePath: string): string {\n\tconst value = normalizeAnthropicBaseUrl(baseUrl, fallbackBasePath);\n\treturn value.endsWith(\"/v1\") ? value.slice(0, -3) : value;\n}\n\nfunction openCodeBaseUrlForApi(api: Api, basePath: string): string {\n\treturn api === \"anthropic-messages\" ? basePath : `${basePath}/v1`;\n}\n\nfunction openCodeModelManagerOptions(\n\tproviderId: \"opencode-go\" | \"opencode-zen\",\n\tconfig?: OpenCodeModelManagerConfig,\n): ModelManagerOptions<Api> {\n\tconst apiKey = config?.apiKey;\n\tconst defaultBaseUrl = getDefaultModelDiscoveryBaseUrl(providerId)!;\n\tconst defaultBasePath = defaultBaseUrl.endsWith(\"/v1\") ? defaultBaseUrl.slice(0, -3) : defaultBaseUrl;\n\tconst basePath = normalizeOpenCodeBasePath(config?.baseUrl, defaultBasePath);\n\tconst discoveryBaseUrl = openCodeBaseUrlForApi(\"openai-completions\", basePath);\n\tconst references = createBundledReferenceMap<Api>(providerId);\n\treturn {\n\t\tproviderId,\n\t\tcacheProviderId: resolveModelCacheProviderId(providerId, { apiKey, baseUrl: discoveryBaseUrl }),\n\t\tdynamicModelsAuthoritative: true,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels<Api>({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: providerId,\n\t\t\t\t\tbaseUrl: discoveryBaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\tconst name = toModelName(entry.name, reference?.name ?? defaults.name);\n\t\t\t\t\t\tif (!reference) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...reference,\n\t\t\t\t\t\t\tid: defaults.id,\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tbaseUrl: openCodeBaseUrlForApi(reference.api, basePath),\n\t\t\t\t\t\t\tcontextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),\n\t\t\t\t\t\t\tmaxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\nexport function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {\n\treturn openCodeModelManagerOptions(\"opencode-zen\", config);\n}\n\nexport function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {\n\treturn openCodeModelManagerOptions(\"opencode-go\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 9. Ollama\n// ---------------------------------------------------------------------------\n\nexport interface OllamaModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function ollamaModelManagerOptions(config?: OllamaModelManagerConfig): ModelManagerOptions<\"openai-responses\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = normalizeOllamaBaseUrl(config?.baseUrl);\n\tconst nativeBaseUrl = toOllamaNativeBaseUrl(baseUrl);\n\tconst references = createBundledReferenceMap<\"openai-responses\">(\"ollama\" as Parameters<typeof getBundledModels>[0]);\n\tconst resolveMetadata = createOllamaMetadataResolver(nativeBaseUrl, config?.fetch);\n\treturn {\n\t\tproviderId: \"ollama\",\n\t\tcacheProviderId: resolveModelCacheProviderId(\"ollama\", { baseUrl }),\n\t\tfetchDynamicModels: async () => {\n\t\t\tconst openAiCompatible = await fetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-responses\",\n\t\t\t\tprovider: \"ollama\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\tif (!reference) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\t\tname: toModelName(entry.name, defaults.name),\n\t\t\t\t\t\t\tcontextWindow: OLLAMA_FALLBACK_CONTEXT_WINDOW,\n\t\t\t\t\t\t\tmaxTokens: OLLAMA_DEFAULT_MAX_TOKENS,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\treturn mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t});\n\t\t\tif (openAiCompatible && openAiCompatible.length > 0) {\n\t\t\t\tawait Promise.all(\n\t\t\t\t\topenAiCompatible.map(async model => {\n\t\t\t\t\t\tconst metadata = await resolveMetadata(model.id);\n\t\t\t\t\t\tmodel.contextWindow = metadata.contextWindow;\n\t\t\t\t\t\tif (metadata.reasoning !== undefined) {\n\t\t\t\t\t\t\tmodel.reasoning = metadata.reasoning;\n\t\t\t\t\t\t\tmodel.thinking = metadata.thinking;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (metadata.input) {\n\t\t\t\t\t\t\tmodel.input = metadata.input;\n\t\t\t\t\t\t}\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn openAiCompatible;\n\t\t\t}\n\t\t\tconst nativeFallback = await fetchOllamaNativeModels(baseUrl, resolveMetadata, config?.fetch);\n\t\t\tif (nativeFallback && nativeFallback.length > 0) {\n\t\t\t\treturn nativeFallback;\n\t\t\t}\n\t\t\treturn openAiCompatible;\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 10. OpenRouter\n// ---------------------------------------------------------------------------\n\nexport interface OpenRouterModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function openrouterModelManagerOptions(\n\tconfig?: OpenRouterModelManagerConfig,\n): ModelManagerOptions<\"openrouter\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://openrouter.ai/api/v1\";\n\tconst references = createBundledReferenceMap<\"openrouter\">(\"openrouter\");\n\treturn {\n\t\tproviderId: \"openrouter\",\n\t\t// Older builds cached OpenRouter discovery rows as `api: \"openai-completions\"`.\n\t\t// Namespace the refreshed pseudo-API cache separately so those rows cannot\n\t\t// override bundled `api: \"openrouter\"` models during online-if-uncached startup.\n\t\tcacheProviderId: resolveModelCacheProviderId(\"openrouter\"),\n\t\tfetchDynamicModels: () =>\n\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openrouter\",\n\t\t\t\tprovider: \"openrouter\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tfilterModel: (entry: OpenAICompatibleModelRecord) => {\n\t\t\t\t\tconst params = entry.supported_parameters;\n\t\t\t\t\treturn Array.isArray(params) && params.includes(\"tools\");\n\t\t\t\t},\n\t\t\t\tmapModel: (\n\t\t\t\t\tentry: OpenAICompatibleModelRecord,\n\t\t\t\t\tdefaults: ModelSpec<\"openrouter\">,\n\t\t\t\t\t_context: OpenAICompatibleModelMapperContext<\"openrouter\">,\n\t\t\t\t): ModelSpec<\"openrouter\"> => {\n\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\tconst baseModel = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\tconst pricing = entry.pricing as Record<string, unknown> | undefined;\n\t\t\t\t\tconst params = Array.isArray(entry.supported_parameters) ? (entry.supported_parameters as string[]) : [];\n\t\t\t\t\tconst modality = String((entry.architecture as Record<string, unknown> | undefined)?.modality ?? \"\");\n\t\t\t\t\tconst topProvider = entry.top_provider as Record<string, unknown> | undefined;\n\n\t\t\t\t\tconst supportsToolChoice = params.includes(\"tool_choice\");\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...baseModel,\n\t\t\t\t\t\treasoning: params.includes(\"reasoning\"),\n\t\t\t\t\t\tinput: modality.includes(\"image\") ? [\"text\", \"image\"] : [\"text\"],\n\t\t\t\t\t\tcost: {\n\t\t\t\t\t\t\tinput: parseFloat(String(pricing?.prompt ?? \"0\")) * 1_000_000,\n\t\t\t\t\t\t\toutput: parseFloat(String(pricing?.completion ?? \"0\")) * 1_000_000,\n\t\t\t\t\t\t\tcacheRead: parseFloat(String(pricing?.input_cache_read ?? \"0\")) * 1_000_000,\n\t\t\t\t\t\t\tcacheWrite: parseFloat(String(pricing?.input_cache_write ?? \"0\")) * 1_000_000,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcontextWindow:\n\t\t\t\t\t\t\ttypeof entry.context_length === \"number\" ? entry.context_length : baseModel.contextWindow,\n\t\t\t\t\t\tmaxTokens:\n\t\t\t\t\t\t\ttypeof topProvider?.max_completion_tokens === \"number\"\n\t\t\t\t\t\t\t\t? topProvider.max_completion_tokens\n\t\t\t\t\t\t\t\t: baseModel.maxTokens,\n\t\t\t\t\t\t...(!supportsToolChoice && {\n\t\t\t\t\t\t\tcompat: { ...(baseModel.compat ?? {}), supportsToolChoice: false },\n\t\t\t\t\t\t}),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t}),\n\t};\n}\n\nconst ZENMUX_OPENAI_BASE_URL = \"https://zenmux.ai/api/v1\";\nconst ZENMUX_ANTHROPIC_BASE_URL = \"https://zenmux.ai/api/anthropic\";\n\nfunction normalizeZenMuxOpenAiBaseUrl(baseUrl?: string): string {\n\tconst value = baseUrl?.trim();\n\tif (!value) {\n\t\treturn ZENMUX_OPENAI_BASE_URL;\n\t}\n\tconst normalized = value.endsWith(\"/\") ? value.slice(0, -1) : value;\n\tif (normalized.endsWith(\"/api/anthropic\")) {\n\t\treturn normalized.replace(\"/api/anthropic\", \"/api/v1\");\n\t}\n\treturn normalized;\n}\n\nfunction toZenMuxAnthropicBaseUrl(openAiBaseUrl: string): string {\n\ttry {\n\t\tconst parsed = new URL(openAiBaseUrl);\n\t\tconst trimmedPath = parsed.pathname.replace(/\\/+$/g, \"\");\n\t\tparsed.pathname = trimmedPath.endsWith(\"/api/v1\")\n\t\t\t? `${trimmedPath.slice(0, -\"/api/v1\".length)}/api/anthropic`\n\t\t\t: \"/api/anthropic\";\n\t\treturn `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n\t} catch {\n\t\treturn ZENMUX_ANTHROPIC_BASE_URL;\n\t}\n}\n\nfunction isZenMuxAnthropicModel(entry: OpenAICompatibleModelRecord, modelId: string): boolean {\n\tif (typeof entry.owned_by === \"string\" && entry.owned_by.toLowerCase() === \"anthropic\") {\n\t\treturn true;\n\t}\n\treturn modelId.toLowerCase().startsWith(\"anthropic/\");\n}\n\nfunction getZenMuxPricingValue(pricings: Record<string, unknown> | undefined, key: string): number {\n\tconst bucket = pricings?.[key];\n\tif (!Array.isArray(bucket)) {\n\t\treturn 0;\n\t}\n\tfor (const item of bucket) {\n\t\tif (!isRecord(item)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst value = toNumber(item.value);\n\t\tif (value !== undefined) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn 0;\n}\n\nfunction getZenMuxCacheWritePrice(pricings: Record<string, unknown> | undefined): number {\n\tconst oneHour = getZenMuxPricingValue(pricings, \"input_cache_write_1_h\");\n\tif (oneHour > 0) {\n\t\treturn oneHour;\n\t}\n\tconst fiveMinute = getZenMuxPricingValue(pricings, \"input_cache_write_5_min\");\n\tif (fiveMinute > 0) {\n\t\treturn fiveMinute;\n\t}\n\treturn getZenMuxPricingValue(pricings, \"input_cache_write\");\n}\n\n// ---------------------------------------------------------------------------\n// 10.5 ZenMux\n// ---------------------------------------------------------------------------\n\nexport interface ZenMuxModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function zenmuxModelManagerOptions(config?: ZenMuxModelManagerConfig): ModelManagerOptions<Api> {\n\tconst apiKey = config?.apiKey;\n\tconst openAiBaseUrl = normalizeZenMuxOpenAiBaseUrl(config?.baseUrl);\n\tconst anthropicBaseUrl = toZenMuxAnthropicBaseUrl(openAiBaseUrl);\n\treturn {\n\t\tproviderId: \"zenmux\",\n\t\tfetchDynamicModels: () =>\n\t\t\tfetchOpenAICompatibleModels<Api>({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"zenmux\",\n\t\t\t\tbaseUrl: openAiBaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\tconst pricings = isRecord(entry.pricings) ? entry.pricings : undefined;\n\t\t\t\t\tconst capabilities = isRecord(entry.capabilities) ? entry.capabilities : undefined;\n\t\t\t\t\tconst isAnthropicModel = isZenMuxAnthropicModel(entry, defaults.id);\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\tname: toModelName(entry.display_name, defaults.name),\n\t\t\t\t\t\tapi: isAnthropicModel ? \"anthropic-messages\" : \"openai-completions\",\n\t\t\t\t\t\tbaseUrl: isAnthropicModel ? anthropicBaseUrl : openAiBaseUrl,\n\t\t\t\t\t\treasoning: capabilities?.reasoning === true || defaults.reasoning,\n\t\t\t\t\t\tinput: toInputCapabilities(entry.input_modalities),\n\t\t\t\t\t\tcost: {\n\t\t\t\t\t\t\tinput: getZenMuxPricingValue(pricings, \"prompt\"),\n\t\t\t\t\t\t\toutput: getZenMuxPricingValue(pricings, \"completion\"),\n\t\t\t\t\t\t\tcacheRead: getZenMuxPricingValue(pricings, \"input_cache_read\"),\n\t\t\t\t\t\t\tcacheWrite: getZenMuxCacheWritePrice(pricings),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcontextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),\n\t\t\t\t\t\tmaxTokens: toPositiveNumber(entry.max_completion_tokens, defaults.maxTokens),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 10.6 Kilo Gateway\n// ---------------------------------------------------------------------------\n\nexport interface KiloModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function kiloModelManagerOptions(config?: KiloModelManagerConfig): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.kilo.ai/api/gateway\";\n\treturn {\n\t\tproviderId: \"kilo\",\n\t\tfetchDynamicModels: () =>\n\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"kilo\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tfetch: config?.fetch,\n\t\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Alibaba Coding Plan\n// ---------------------------------------------------------------------------\n\nexport interface AlibabaCodingPlanModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function alibabaCodingPlanModelManagerOptions(\n\tconfig?: AlibabaCodingPlanModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://coding-intl.dashscope.aliyuncs.com/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"alibaba-coding-plan\");\n\treturn {\n\t\tproviderId: \"alibaba-coding-plan\",\n\t\tfetchDynamicModels: () =>\n\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"alibaba-coding-plan\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\treturn mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Alibaba Token Plan\n// ---------------------------------------------------------------------------\n\nexport { ALIBABA_TOKEN_PLAN_BASE_URL };\n\nconst ALIBABA_TOKEN_PLAN_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;\nconst ALIBABA_TOKEN_PLAN_COMPAT: OpenAICompat = {\n\tsupportsDeveloperRole: false,\n};\nconst ALIBABA_TOKEN_PLAN_REASONING: ThinkingConfig = {\n\tmode: \"effort\",\n\tefforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],\n};\n\nexport const ALIBABA_TOKEN_PLAN_STATIC_MODELS: readonly ModelSpec<\"openai-completions\">[] = [\n\t{\n\t\tid: \"qwen3.8-max-preview\",\n\t\tname: \"Qwen3.8 Max Preview\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"alibaba-token-plan\",\n\t\tbaseUrl: ALIBABA_TOKEN_PLAN_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\", \"image\"],\n\t\tcost: ALIBABA_TOKEN_PLAN_COST,\n\t\tcontextWindow: 983_616,\n\t\tmaxTokens: 131_072,\n\t\tthinking: {\n\t\t\tmode: \"effort\",\n\t\t\tefforts: [Effort.Low, Effort.High, Effort.XHigh],\n\t\t\trequiresEffort: true,\n\t\t},\n\t\tcompat: {\n\t\t\t...ALIBABA_TOKEN_PLAN_COMPAT,\n\t\t\tsupportsReasoningEffort: true,\n\t\t},\n\t},\n\t{\n\t\tid: \"qwen3.7-max\",\n\t\tname: \"Qwen3.7 Max\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"alibaba-token-plan\",\n\t\tbaseUrl: ALIBABA_TOKEN_PLAN_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\"],\n\t\tcost: ALIBABA_TOKEN_PLAN_COST,\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 65_536,\n\t\tthinking: ALIBABA_TOKEN_PLAN_REASONING,\n\t\tcompat: ALIBABA_TOKEN_PLAN_COMPAT,\n\t},\n\t{\n\t\tid: \"qwen3.7-plus\",\n\t\tname: \"Qwen3.7 Plus\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"alibaba-token-plan\",\n\t\tbaseUrl: ALIBABA_TOKEN_PLAN_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\", \"image\"],\n\t\tcost: ALIBABA_TOKEN_PLAN_COST,\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 64_000,\n\t\tthinking: ALIBABA_TOKEN_PLAN_REASONING,\n\t\tcompat: ALIBABA_TOKEN_PLAN_COMPAT,\n\t},\n\t{\n\t\tid: \"qwen3.6-flash\",\n\t\tname: \"Qwen3.6 Flash\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"alibaba-token-plan\",\n\t\tbaseUrl: ALIBABA_TOKEN_PLAN_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\", \"image\"],\n\t\tcost: ALIBABA_TOKEN_PLAN_COST,\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 65_536,\n\t\tthinking: ALIBABA_TOKEN_PLAN_REASONING,\n\t\tcompat: ALIBABA_TOKEN_PLAN_COMPAT,\n\t},\n\t{\n\t\tid: \"glm-5.2\",\n\t\tname: \"GLM-5.2\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"alibaba-token-plan\",\n\t\tbaseUrl: ALIBABA_TOKEN_PLAN_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\"],\n\t\tcost: ALIBABA_TOKEN_PLAN_COST,\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 131_072,\n\t\tthinking: {\n\t\t\tmode: \"effort\",\n\t\t\tefforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.Max],\n\t\t},\n\t\tcompat: ALIBABA_TOKEN_PLAN_COMPAT,\n\t},\n\t{\n\t\tid: \"deepseek-v4-pro\",\n\t\tname: \"DeepSeek V4 Pro\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"alibaba-token-plan\",\n\t\tbaseUrl: ALIBABA_TOKEN_PLAN_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\"],\n\t\tcost: ALIBABA_TOKEN_PLAN_COST,\n\t\tcontextWindow: 1_000_000,\n\t\tmaxTokens: 384_000,\n\t\tthinking: {\n\t\t\tmode: \"effort\",\n\t\t\tefforts: [Effort.High, Effort.Max],\n\t\t},\n\t\tcompat: ALIBABA_TOKEN_PLAN_COMPAT,\n\t},\n];\n\nexport interface AlibabaTokenPlanModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function alibabaTokenPlanModelManagerOptions(\n\tconfig?: AlibabaTokenPlanModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst credential = config?.apiKey ? parseAlibabaTokenPlanCredential(config.apiKey) : undefined;\n\tconst apiKey = credential?.token;\n\t// A region-locked credential (China/custom) dictates the discovery endpoint:\n\t// its key only authenticates against its own region, so fetching /models from\n\t// any other base URL would 401 (#6682).\n\tconst baseUrl = credential?.baseUrl ?? config?.baseUrl ?? ALIBABA_TOKEN_PLAN_BASE_URL;\n\treturn {\n\t\tproviderId: \"alibaba-token-plan\",\n\t\tdynamicModelsAuthoritative: true,\n\t\tstaticModels: ALIBABA_TOKEN_PLAN_STATIC_MODELS,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"alibaba-token-plan\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tfilterModel: (_entry, model) =>\n\t\t\t\t\t\tALIBABA_TOKEN_PLAN_STATIC_MODELS.some(reference => reference.id === model.id),\n\t\t\t\t\tmapModel: (_entry, defaults) => {\n\t\t\t\t\t\tconst reference = ALIBABA_TOKEN_PLAN_STATIC_MODELS.find(model => model.id === defaults.id);\n\t\t\t\t\t\treturn reference\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t...reference,\n\t\t\t\t\t\t\t\t\tid: defaults.id,\n\t\t\t\t\t\t\t\t\tapi: defaults.api,\n\t\t\t\t\t\t\t\t\tprovider: defaults.provider,\n\t\t\t\t\t\t\t\t\tbaseUrl: defaults.baseUrl,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: defaults;\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 11. Vercel AI Gateway\n// ---------------------------------------------------------------------------\n\nexport interface VercelAiGatewayModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nfunction normalizeVercelAiGatewayBaseUrls(rawBaseUrl: string | undefined): { baseUrl: string; catalogBaseUrl: string } {\n\tconst baseUrl = (rawBaseUrl === undefined ? \"https://ai-gateway.vercel.sh\" : rawBaseUrl.trim()).replace(/\\/+$/, \"\");\n\tconst catalogBaseUrl = baseUrl === \"\" || baseUrl.endsWith(\"/v1\") ? baseUrl : `${baseUrl}/v1`;\n\n\treturn {\n\t\tbaseUrl: baseUrl.endsWith(\"/v1\") ? baseUrl.slice(0, -3) : baseUrl,\n\t\tcatalogBaseUrl,\n\t};\n}\n\nexport function vercelAiGatewayModelManagerOptions(\n\tconfig?: VercelAiGatewayModelManagerConfig,\n): ModelManagerOptions<\"anthropic-messages\"> {\n\tconst apiKey = config?.apiKey;\n\tconst { baseUrl, catalogBaseUrl } = normalizeVercelAiGatewayBaseUrls(config?.baseUrl);\n\treturn {\n\t\tproviderId: \"vercel-ai-gateway\",\n\t\tfetchDynamicModels: () =>\n\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\tapi: \"anthropic-messages\",\n\t\t\t\tprovider: \"vercel-ai-gateway\",\n\t\t\t\tbaseUrl: catalogBaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tfilterModel: (entry: OpenAICompatibleModelRecord) => {\n\t\t\t\t\tconst tags = entry.tags;\n\t\t\t\t\treturn Array.isArray(tags) && tags.includes(\"tool-use\");\n\t\t\t\t},\n\t\t\t\tmapModel: (\n\t\t\t\t\tentry: OpenAICompatibleModelRecord,\n\t\t\t\t\tdefaults: ModelSpec<\"anthropic-messages\">,\n\t\t\t\t\t_context: OpenAICompatibleModelMapperContext<\"anthropic-messages\">,\n\t\t\t\t): ModelSpec<\"anthropic-messages\"> => {\n\t\t\t\t\tconst pricing = entry.pricing as Record<string, unknown> | undefined;\n\t\t\t\t\tconst tags = Array.isArray(entry.tags) ? (entry.tags as string[]) : [];\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\tbaseUrl,\n\t\t\t\t\t\treasoning: tags.includes(\"reasoning\"),\n\t\t\t\t\t\tinput: tags.includes(\"vision\") ? [\"text\", \"image\"] : [\"text\"],\n\t\t\t\t\t\tcost: {\n\t\t\t\t\t\t\tinput: (toNumber(pricing?.input) ?? 0) * 1_000_000,\n\t\t\t\t\t\t\toutput: (toNumber(pricing?.output) ?? 0) * 1_000_000,\n\t\t\t\t\t\t\tcacheRead: (toNumber(pricing?.input_cache_read) ?? 0) * 1_000_000,\n\t\t\t\t\t\t\tcacheWrite: (toNumber(pricing?.input_cache_write) ?? 0) * 1_000_000,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcontextWindow:\n\t\t\t\t\t\t\ttypeof entry.context_window === \"number\" ? entry.context_window : defaults.contextWindow,\n\t\t\t\t\t\tmaxTokens: typeof entry.max_tokens === \"number\" ? entry.max_tokens : defaults.maxTokens,\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 12. Kimi Code\n// ---------------------------------------------------------------------------\n\nexport interface KimiCodeModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nfunction mapKimiThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {\n\tconst raw = entry.think_efforts;\n\tif (!isRecord(raw) || raw.support !== true) return undefined;\n\tconst validEfforts = raw.valid_efforts;\n\tif (!Array.isArray(validEfforts)) return undefined;\n\tconst efforts = THINKING_EFFORTS.filter(effort => validEfforts.includes(effort));\n\tif (efforts.length === 0) return undefined;\n\n\tconst thinking: ThinkingConfig = { mode: \"effort\", efforts };\n\tif (entry.supports_thinking_type === \"only\") {\n\t\tthinking.requiresEffort = true;\n\t}\n\tif (typeof raw.default_effort === \"string\") {\n\t\tconst defaultLevel = THINKING_EFFORTS.find(effort => effort === raw.default_effort);\n\t\tif (defaultLevel !== undefined && efforts.includes(defaultLevel)) {\n\t\t\tthinking.defaultLevel = defaultLevel;\n\t\t}\n\t}\n\treturn thinking;\n}\n\nfunction kimiSupportsReasoning(entry: OpenAICompatibleModelRecord, modelId: string): boolean {\n\tswitch (entry.supports_thinking_type) {\n\t\tcase \"only\":\n\t\tcase \"both\":\n\t\t\treturn true;\n\t\tcase \"no\":\n\t\t\treturn false;\n\t\tdefault:\n\t\t\treturn entry.supports_reasoning === true || modelId.includes(\"thinking\");\n\t}\n}\n\nfunction mapKimiApiFormat(protocol: unknown): OpenAICompat[\"kimiApiFormat\"] {\n\tif (protocol === \"anthropic\") return \"anthropic\";\n\tif (protocol === null) return \"openai\";\n\treturn undefined;\n}\n\n/**\n * Kimi Code output ceilings by model family. The `/coding/v1/models` discovery\n * envelope carries no output-limit field, so the mapper supplies the documented\n * per-family caps instead of a blanket constant. Values match models.dev's\n * `kimi-for-coding` and `moonshotai` kimi-k3 entries. See #6711.\n */\nexport const KIMI_CODE_K3_MAX_TOKENS = 131_072;\nexport const KIMI_CODE_FOR_CODING_MAX_TOKENS = 32_768;\n\n/** Fallback output cap for Kimi Code families without a documented ceiling (legacy K2 discovery rows). */\nexport const KIMI_CODE_DEFAULT_MAX_TOKENS = 32_000;\n\n/**\n * Resolve a Kimi Code model's output ceiling from its id: `k3` / `k3-256k` ->\n * 131072, `kimi-for-coding[-highspeed]` -> 32768, everything else -> `fallback`.\n */\nexport function kimiCodeMaxTokens(modelId: string, fallback?: number): number;\nexport function kimiCodeMaxTokens(modelId: string, fallback: number | null): number | null;\nexport function kimiCodeMaxTokens(\n\tmodelId: string,\n\tfallback: number | null = KIMI_CODE_DEFAULT_MAX_TOKENS,\n): number | null {\n\tconst id = modelId.toLowerCase();\n\tif (id.startsWith(\"k3\")) return KIMI_CODE_K3_MAX_TOKENS;\n\tif (id.startsWith(\"kimi-for-coding\")) return KIMI_CODE_FOR_CODING_MAX_TOKENS;\n\treturn fallback;\n}\n\nexport function kimiCodeModelManagerOptions(\n\tconfig?: KimiCodeModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.kimi.com/coding/v1\";\n\treturn {\n\t\tproviderId: \"kimi-code\",\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"kimi-code\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"User-Agent\": \"KimiCLI/1.0\",\n\t\t\t\t\t\t\"X-Msh-Platform\": \"kimi_cli\",\n\t\t\t\t\t},\n\t\t\t\t\tmapModel: (\n\t\t\t\t\t\tentry: OpenAICompatibleModelRecord,\n\t\t\t\t\t\tdefaults: ModelSpec<\"openai-completions\">,\n\t\t\t\t\t\t_context: OpenAICompatibleModelMapperContext<\"openai-completions\">,\n\t\t\t\t\t): ModelSpec<\"openai-completions\"> => {\n\t\t\t\t\t\tconst id = defaults.id;\n\t\t\t\t\t\tconst reasoning = kimiSupportsReasoning(entry, id);\n\t\t\t\t\t\tconst thinking = reasoning ? mapKimiThinking(entry) : undefined;\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\t\tname: typeof entry.display_name === \"string\" ? entry.display_name : defaults.name,\n\t\t\t\t\t\t\treasoning,\n\t\t\t\t\t\t\tinput: entry.supports_image_in === true || id.includes(\"k2.5\") ? [\"text\", \"image\"] : [\"text\"],\n\t\t\t\t\t\t\tcontextWindow: typeof entry.context_length === \"number\" ? entry.context_length : 262144,\n\t\t\t\t\t\t\tmaxTokens: kimiCodeMaxTokens(id),\n\t\t\t\t\t\t\tthinking,\n\t\t\t\t\t\t\tcompat: {\n\t\t\t\t\t\t\t\tthinkingFormat: thinking ? \"kimi\" : \"zai\",\n\t\t\t\t\t\t\t\tkimiApiFormat: mapKimiApiFormat(entry.protocol),\n\t\t\t\t\t\t\t\treasoningContentField: \"reasoning_content\",\n\t\t\t\t\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 12.5. LM Studio\n// ---------------------------------------------------------------------------\n\n/** Native LM Studio metadata keyed by model id from `/api/v0/models`. */\nexport interface LmStudioNativeModelMetadata {\n\tinput: (\"text\" | \"image\")[];\n\tcontextWindow?: number;\n}\n\n/** Options for LM Studio's optional native metadata probe. */\nexport interface LmStudioNativeModelMetadataOptions {\n\theaders?: Record<string, string>;\n\tsignal?: AbortSignal;\n}\n\nconst LM_STUDIO_NATIVE_METADATA_TIMEOUT_MS = 250;\n\nfunction toLmStudioNativeBaseUrl(baseUrl: string): string {\n\tconst trimmed = baseUrl.trim();\n\tconst normalized = trimmed.endsWith(\"/\") ? trimmed.slice(0, -1) : trimmed;\n\treturn normalized.endsWith(\"/v1\") ? normalized.slice(0, -3) : normalized;\n}\n\nfunction getLmStudioCapabilityNames(value: unknown): string[] {\n\tif (!Array.isArray(value)) {\n\t\treturn [];\n\t}\n\treturn value.flatMap(item => (typeof item === \"string\" ? [item.toLowerCase()] : []));\n}\n\nfunction getLmStudioNativeInput(entry: Record<string, unknown>): (\"text\" | \"image\")[] {\n\tconst modelType = typeof entry.type === \"string\" ? entry.type.toLowerCase() : \"\";\n\tconst capabilities = getLmStudioCapabilityNames(entry.capabilities);\n\tconst supportsImage = modelType === \"vlm\" || capabilities.includes(\"vision\") || capabilities.includes(\"image\");\n\treturn supportsImage ? [\"text\", \"image\"] : [\"text\"];\n}\n\nfunction getLmStudioNativeContextWindow(entry: Record<string, unknown>): number | undefined {\n\t// LM Studio serves `loaded_context_length` (the window the running instance\n\t// actually accepts) alongside `max_context_length` (the architectural\n\t// ceiling). A model is routinely loaded below its maximum — the user picks a\n\t// smaller window, or MLX context auto-fit shrinks it to fit unified memory —\n\t// so prefer what the runtime serves, matching the Ollama/llama.cpp rule from\n\t// #3754. Unloaded models report `loaded_context_length: null` and fall\n\t// through to the max/train chain unchanged.\n\tconst loadedContextWindow = entry.state === \"loaded\" ? toPositiveNumber(entry.loaded_context_length, null) : null;\n\treturn (\n\t\tloadedContextWindow ??\n\t\ttoPositiveNumber(entry.max_context_length, null) ??\n\t\ttoPositiveNumber(entry.context_length, null) ??\n\t\ttoPositiveNumber(entry.max_model_len, null) ??\n\t\tundefined\n\t);\n}\n\n/** Fetches LM Studio native model metadata used to mark VLM models as image-capable. */\nexport async function fetchLmStudioNativeModelMetadata(\n\tbaseUrl: string,\n\tfetchImpl: FetchImpl = fetch,\n\toptions?: LmStudioNativeModelMetadataOptions,\n): Promise<Map<string, LmStudioNativeModelMetadata> | null> {\n\tconst nativeBaseUrl = toLmStudioNativeBaseUrl(baseUrl);\n\tconst fetchMetadata = async (signal?: AbortSignal): Promise<Map<string, LmStudioNativeModelMetadata> | null> => {\n\t\ttry {\n\t\t\tconst response = await fetchImpl(`${nativeBaseUrl}/api/v0/models`, {\n\t\t\t\tmethod: \"GET\",\n\t\t\t\theaders: { Accept: \"application/json\", ...(options?.headers ?? {}) },\n\t\t\t\tsignal,\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst payload = await response.json();\n\t\t\tif (!isRecord(payload) || !Array.isArray(payload.data)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst metadata = new Map<string, LmStudioNativeModelMetadata>();\n\t\t\tfor (const entry of payload.data) {\n\t\t\t\tif (!isRecord(entry) || typeof entry.id !== \"string\" || entry.id.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst contextWindow = getLmStudioNativeContextWindow(entry);\n\t\t\t\tmetadata.set(entry.id, {\n\t\t\t\t\tinput: getLmStudioNativeInput(entry),\n\t\t\t\t\t...(contextWindow === undefined ? {} : { contextWindow }),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn metadata;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t};\n\tif (options?.signal !== undefined) {\n\t\treturn fetchMetadata(options.signal);\n\t}\n\treturn withCatalogDiscoveryTimeout(LM_STUDIO_NATIVE_METADATA_TIMEOUT_MS, fetchMetadata);\n}\n\nexport interface LmStudioModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function lmStudioModelManagerOptions(\n\tconfig?: LmStudioModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? Bun.env.LM_STUDIO_BASE_URL ?? \"http://127.0.0.1:1234/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"lm-studio\" as any);\n\treturn {\n\t\tproviderId: \"lm-studio\",\n\t\tfetchDynamicModels: async () => {\n\t\t\tconst nativeMetadataPromise = fetchLmStudioNativeModelMetadata(baseUrl, config?.fetch, {\n\t\t\t\theaders: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,\n\t\t\t});\n\t\t\tconst models = await fetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"lm-studio\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\treturn mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t});\n\t\t\tif (!models) {\n\t\t\t\treturn models;\n\t\t\t}\n\t\t\tconst nativeMetadata = await nativeMetadataPromise;\n\t\t\tif (!nativeMetadata) {\n\t\t\t\treturn models;\n\t\t\t}\n\t\t\treturn models.map(model => {\n\t\t\t\tconst metadata = nativeMetadata.get(model.id);\n\t\t\t\tif (!metadata) {\n\t\t\t\t\treturn model;\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\t...model,\n\t\t\t\t\tinput: metadata.input,\n\t\t\t\t\tcontextWindow: metadata.contextWindow ?? model.contextWindow,\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 13. Synthetic\n// ---------------------------------------------------------------------------\n\nexport interface SyntheticModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\n/**\n * Synthetic's `/openai/v1/models` entry shape (verified live against\n * api.synthetic.new). It shares no capability field names with the generic\n * OpenAI-compatible conventions: capabilities arrive in `supported_features`,\n * modalities in `input_modalities`, the output cap in `max_output_length`, the\n * accepted `reasoning_effort` vocabulary in `reasoning_parameters.efforts`,\n * and per-token prices in `pricing` as `$`-prefixed decimal strings.\n */\ninterface SyntheticModelRecord extends OpenAICompatibleModelRecord {\n\tsupported_features?: unknown;\n\tinput_modalities?: unknown;\n\tmax_output_length?: unknown;\n\treasoning_parameters?: unknown;\n\tpricing?: unknown;\n}\n\n/** Synthetic's thinking-off wire tier — a router state, not a user effort. */\nconst SYNTHETIC_WIRE_EFFORT_NONE = \"none\";\n/** Output cap for routes that advertise no `max_output_length`. */\nconst SYNTHETIC_FALLBACK_MAX_TOKENS = 8192;\n\nfunction toSyntheticStringList(value: unknown): readonly string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\n/**\n * Translate Synthetic's per-model `reasoning_effort` vocabulary into an effort\n * ladder. Every advertised value that names an OMP tier maps verbatim; `none`\n * is the thinking-off state rather than a tier of its own, so it backs the\n * `minimal` selector through the wire map (same shape as the Fireworks\n * `minimal → none` map) and gives these routes a real no-thinking tier.\n * A route that advertises only `none` (or tiers this client doesn't know)\n * still gets the minimal-off mapping: falling through to identity inference\n * would fabricate an unadvertised ladder, and leaving thinking unset would\n * leak any stale reference ladder past the wire vocabulary.\n */\nfunction resolveSyntheticThinking(wireEfforts: readonly string[]): ThinkingConfig | undefined {\n\tconst efforts = THINKING_EFFORTS.filter(effort => wireEfforts.includes(effort));\n\tconst wireHasNone = wireEfforts.includes(SYNTHETIC_WIRE_EFFORT_NONE);\n\tif (efforts.length === 0) {\n\t\treturn wireHasNone\n\t\t\t? { mode: \"effort\", efforts: [Effort.Minimal], effortMap: { [Effort.Minimal]: SYNTHETIC_WIRE_EFFORT_NONE } }\n\t\t\t: undefined;\n\t}\n\tif (!wireHasNone || efforts.includes(Effort.Minimal)) {\n\t\treturn { mode: \"effort\", efforts };\n\t}\n\treturn {\n\t\tmode: \"effort\",\n\t\tefforts: [Effort.Minimal, ...efforts],\n\t\teffortMap: { [Effort.Minimal]: SYNTHETIC_WIRE_EFFORT_NONE },\n\t};\n}\n\n/** Synthetic quotes per-token USD as `\"$0.000001\"`; catalog cost is per-million. */\nfunction toSyntheticCostPerMillion(value: unknown): number | undefined {\n\tconst parsed = toNumber(typeof value === \"string\" ? value.trim().replace(/^\\$/, \"\") : value);\n\tif (parsed === undefined || parsed < 0) {\n\t\treturn undefined;\n\t}\n\t// Scaling a per-token decimal by 1e6 drifts (4.5e-7 → 0.44999999999999996), so\n\t// settle on a millionth of a dollar per million tokens — finer than any real tier.\n\treturn Math.round(parsed * 1e12) / 1e6;\n}\n\nfunction resolveSyntheticCost(\n\tpricing: unknown,\n\tfallback: ModelSpec<\"openai-completions\">[\"cost\"],\n): ModelSpec<\"openai-completions\">[\"cost\"] {\n\tif (!isRecord(pricing)) {\n\t\treturn fallback;\n\t}\n\tconst input = toSyntheticCostPerMillion(pricing.prompt);\n\tconst output = toSyntheticCostPerMillion(pricing.completion);\n\tif (input === undefined || output === undefined) {\n\t\treturn fallback;\n\t}\n\treturn {\n\t\tinput,\n\t\toutput,\n\t\tcacheRead: toSyntheticCostPerMillion(pricing.input_cache_reads) ?? fallback.cacheRead,\n\t\tcacheWrite: toSyntheticCostPerMillion(pricing.input_cache_writes) ?? fallback.cacheWrite,\n\t};\n}\n\nexport function syntheticModelManagerOptions(\n\tconfig?: SyntheticModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.synthetic.new/openai/v1\";\n\tconst references = new Map(\n\t\t(getBundledModels(\"synthetic\") as Model<\"openai-completions\">[]).map(model => [model.id, toModelSpec(model)]),\n\t);\n\treturn {\n\t\tproviderId: \"synthetic\",\n\t\tdynamicModelsAuthoritative: true,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"synthetic\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (\n\t\t\t\t\t\tentry: OpenAICompatibleModelRecord,\n\t\t\t\t\t\tdefaults: ModelSpec<\"openai-completions\">,\n\t\t\t\t\t\t_context: OpenAICompatibleModelMapperContext<\"openai-completions\">,\n\t\t\t\t\t): ModelSpec<\"openai-completions\"> => {\n\t\t\t\t\t\tconst record = entry as SyntheticModelRecord;\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\tconst referenceSupportsImage = reference?.input.includes(\"image\") ?? false;\n\t\t\t\t\t\tconst features = toSyntheticStringList(record.supported_features);\n\t\t\t\t\t\tconst modalities = toSyntheticStringList(record.input_modalities);\n\t\t\t\t\t\tconst wireEfforts = isRecord(record.reasoning_parameters)\n\t\t\t\t\t\t\t? toSyntheticStringList(record.reasoning_parameters.efforts)\n\t\t\t\t\t\t\t: [];\n\t\t\t\t\t\tconst wireReasoning = features.includes(\"reasoning\") || wireEfforts.length > 0;\n\t\t\t\t\t\tconst thinking = resolveSyntheticThinking(wireEfforts);\n\t\t\t\t\t\t// An advertised effort vocabulary is authoritative over the bundled\n\t\t\t\t\t\t// reference: when the wire names tiers (even only `none`), the\n\t\t\t\t\t\t// reference's reasoning flag must not re-add a dial the route\n\t\t\t\t\t\t// doesn't expose. A route with at least one named tier reasons —\n\t\t\t\t\t\t// even a single tier is a real effort the wire accepts. Only a\n\t\t\t\t\t\t// vocabulary of `none`/unrecognized values alone is the pure\n\t\t\t\t\t\t// off-switch: reporting `reasoning: true` there would light up the\n\t\t\t\t\t\t// effort dial for a dial with one stop. When the wire is silent on\n\t\t\t\t\t\t// reasoning entirely, the reference gets a vote.\n\t\t\t\t\t\tconst namedTierCount =\n\t\t\t\t\t\t\t(thinking?.efforts.length ?? 0) - (wireEfforts.includes(SYNTHETIC_WIRE_EFFORT_NONE) ? 1 : 0);\n\t\t\t\t\t\tconst reasoning =\n\t\t\t\t\t\t\twireReasoning && namedTierCount > 0\n\t\t\t\t\t\t\t\t? true\n\t\t\t\t\t\t\t\t: wireEfforts.length > 0\n\t\t\t\t\t\t\t\t\t? false\n\t\t\t\t\t\t\t\t\t: entry.supports_reasoning === true || (reference?.reasoning ?? false);\n\t\t\t\t\t\t// The router aliases (`syn:*`) and newly added routes carry no\n\t\t\t\t\t\t// bundled reference, so these advertised capabilities are the only\n\t\t\t\t\t\t// truth available. Without them such a model lands non-reasoning\n\t\t\t\t\t\t// (which hides the thinking selector and drops `reasoning_effort`\n\t\t\t\t\t\t// from every request), text-only, priced at zero, and capped at the\n\t\t\t\t\t\t// 8k placeholder — a cap low enough that verbose models stop on\n\t\t\t\t\t\t// `length` each turn and trip recovery compaction.\n\t\t\t\t\t\tconst base = reference ? { ...reference, id: defaults.id, baseUrl } : defaults;\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...base,\n\t\t\t\t\t\t\tname: toModelName(entry.name, reference?.name ?? defaults.name),\n\t\t\t\t\t\t\treasoning,\n\t\t\t\t\t\t\t...(thinking ? { thinking } : {}),\n\t\t\t\t\t\t\tinput:\n\t\t\t\t\t\t\t\tmodalities.includes(\"image\") || entry.supports_vision === true || referenceSupportsImage\n\t\t\t\t\t\t\t\t\t? [\"text\", \"image\"]\n\t\t\t\t\t\t\t\t\t: [\"text\"],\n\t\t\t\t\t\t\t// A present `supported_features` list (even empty) is the route's\n\t\t\t\t\t\t\t// whole advertised surface: no `tools` entry means no tool\n\t\t\t\t\t\t\t// support. The reference still wins when it already vouched for\n\t\t\t\t\t\t\t// tools, since a populated wire list can be incomplete; an\n\t\t\t\t\t\t\t// explicit reference `false` stays `false` either way.\n\t\t\t\t\t\t\t...(record.supported_features !== undefined &&\n\t\t\t\t\t\t\t!features.includes(\"tools\") &&\n\t\t\t\t\t\t\treference?.supportsTools !== true\n\t\t\t\t\t\t\t\t? { supportsTools: false }\n\t\t\t\t\t\t\t\t: reference?.supportsTools === false\n\t\t\t\t\t\t\t\t\t? { supportsTools: false }\n\t\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t\tcost: resolveSyntheticCost(record.pricing, base.cost),\n\t\t\t\t\t\t\tcontextWindow: toPositiveNumber(\n\t\t\t\t\t\t\t\tentry.context_length,\n\t\t\t\t\t\t\t\treference?.contextWindow ?? defaults.contextWindow,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tmaxTokens: toPositiveNumber(\n\t\t\t\t\t\t\t\trecord.max_output_length ?? entry.max_tokens,\n\t\t\t\t\t\t\t\treference?.maxTokens ?? SYNTHETIC_FALLBACK_MAX_TOKENS,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 14. Venice\n// ---------------------------------------------------------------------------\n\nexport interface VeniceModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function veniceModelManagerOptions(\n\tconfig?: VeniceModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://api.venice.ai/api/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"venice\");\n\treturn {\n\t\tproviderId: \"venice\",\n\t\tfetchDynamicModels: () =>\n\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"venice\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\tconst model = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...model,\n\t\t\t\t\t\tmaxTokens: clampKimiK27CodeMaxTokens(defaults.id, model.maxTokens),\n\t\t\t\t\t\tcompat: { ...model.compat, supportsUsageInStreaming: false },\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 14.5 Baseten\n// ---------------------------------------------------------------------------\n\nexport interface BasetenModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function basetenModelManagerOptions(\n\tconfig?: BasetenModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://inference.baseten.co/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"baseten\");\n\treturn {\n\t\tproviderId: \"baseten\",\n\t\tdynamicModelsAuthoritative: true,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"baseten\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\tconst raw = entry as Record<string, unknown> & {\n\t\t\t\t\t\t\tsupported_features?: unknown;\n\t\t\t\t\t\t\tinput_modalities?: unknown;\n\t\t\t\t\t\t\tpricing?: Record<string, unknown>;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst features = Array.isArray(raw.supported_features) ? raw.supported_features : [];\n\t\t\t\t\t\tconst modalities = Array.isArray(raw.input_modalities) ? raw.input_modalities : [];\n\n\t\t\t\t\t\tconst isBasetenNativeReasoning =\n\t\t\t\t\t\t\tdefaults.id === \"openai/gpt-oss-120b\" ||\n\t\t\t\t\t\t\tdefaults.id === \"deepseek-ai/DeepSeek-V4-Pro\" ||\n\t\t\t\t\t\t\tdefaults.id === \"zai-org/GLM-5.2\";\n\t\t\t\t\t\tconst reasoning =\n\t\t\t\t\t\t\tisBasetenNativeReasoning &&\n\t\t\t\t\t\t\t(features.includes(\"reasoning\") || features.includes(\"reasoning_effort\"));\n\t\t\t\t\t\tconst supportsTools = features.includes(\"tools\") ? undefined : false;\n\t\t\t\t\t\tconst vision = modalities.includes(\"image\") || (reference?.input.includes(\"image\") ?? false);\n\n\t\t\t\t\t\tconst pricing = raw.pricing ?? {};\n\t\t\t\t\t\tconst cost = {\n\t\t\t\t\t\t\tinput: toPositiveNumber(pricing.prompt, 0) * 1_000_000,\n\t\t\t\t\t\t\toutput: toPositiveNumber(pricing.completion, 0) * 1_000_000,\n\t\t\t\t\t\t\tcacheRead: toPositiveNumber(pricing.input_cache_read, 0) * 1_000_000,\n\t\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst contextWindow = toPositiveNumber(\n\t\t\t\t\t\t\traw.context_length,\n\t\t\t\t\t\t\treference?.contextWindow ?? defaults.contextWindow,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst maxTokens = toPositiveNumber(\n\t\t\t\t\t\t\traw.max_completion_tokens,\n\t\t\t\t\t\t\treference?.maxTokens ?? defaults.maxTokens,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tconst baseModel = mapWithBundledReference(entry, defaults, reference);\n\n\t\t\t\t\t\t// Baseten's reasoning router accepts only the high/max\n\t\t\t\t\t\t// effort tiers for its GLM-5.2 and gpt-oss routes.\n\t\t\t\t\t\tconst isEffortReasoning = defaults.id === \"openai/gpt-oss-120b\" || defaults.id === \"zai-org/GLM-5.2\";\n\t\t\t\t\t\tconst thinking = isEffortReasoning\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tmode: \"effort\" as const,\n\t\t\t\t\t\t\t\t\tefforts: [Effort.High, Effort.Max],\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...baseModel,\n\t\t\t\t\t\t\treasoning,\n\t\t\t\t\t\t\tinput: vision ? [\"text\", \"image\"] : [\"text\"],\n\t\t\t\t\t\t\tcost,\n\t\t\t\t\t\t\tcontextWindow,\n\t\t\t\t\t\t\tmaxTokens,\n\t\t\t\t\t\t\t...(thinking ? { thinking } : {}),\n\t\t\t\t\t\t\t...(supportsTools === false ? { supportsTools } : {}),\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 15. Together\n// ---------------------------------------------------------------------------\n\nexport interface TogetherModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function togetherModelManagerOptions(\n\tconfig?: TogetherModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"together\", \"https://api.together.xyz/v1\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 15.5 CoreWeave Serverless Inference\n// ---------------------------------------------------------------------------\n\nexport interface CoreWeaveModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function coreWeaveModelManagerOptions(\n\tconfig?: CoreWeaveModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"coreweave\", \"https://api.inference.wandb.ai/v1\", {\n\t\t...config,\n\t\theaders: () => coreWeaveProjectHeaders(Bun.env),\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// 15.75 Meta Model API\n// ---------------------------------------------------------------------------\n\nconst META_MODEL_API_BASE_URL = \"https://api.meta.ai/v1\";\nconst META_MUSE_SPARK_COST = { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 } as const;\nconst META_MUSE_SPARK_THINKING: ThinkingConfig = {\n\tmode: \"effort\",\n\tefforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh],\n};\n\nexport const META_MUSE_STATIC_MODELS: readonly ModelSpec<\"openai-responses\">[] = [\n\t{\n\t\tid: \"muse-spark-1.1\",\n\t\tname: \"Muse Spark 1.1\",\n\t\tapi: \"openai-responses\",\n\t\tprovider: \"meta\",\n\t\tbaseUrl: META_MODEL_API_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\", \"image\"],\n\t\tcost: META_MUSE_SPARK_COST,\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 131_072,\n\t\tthinking: META_MUSE_SPARK_THINKING,\n\t\tcompat: {\n\t\t\tsupportsReasoningEffort: true,\n\t\t\tincludeEncryptedReasoning: true,\n\t\t},\n\t},\n];\n\nexport interface MetaModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function metaModelManagerOptions(config?: MetaModelManagerConfig): ModelManagerOptions<\"openai-responses\"> {\n\treturn {\n\t\t...createSimpleOpenAIResponsesOptions(\"meta\", META_MODEL_API_BASE_URL, config),\n\t\tstaticModels: META_MUSE_STATIC_MODELS,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 16. Moonshot\n// ---------------------------------------------------------------------------\n\nexport interface MoonshotModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\n/**\n * Moonshot Kimi K3 discovery metadata. K3 is dynamically discovered but absent\n * from models.dev and the bundled catalog, so `mapWithBundledReference` would\n * otherwise assign zero cost, null limits, text-only input, and no reasoning —\n * mislabeling a paid model as \"Free\" (#5756). Pricing/limits from Moonshot's\n * official chat-k3 pricing and quickstart guide:\n * https://platform.kimi.ai/docs/pricing/chat-k3.md\n * https://platform.kimi.ai/docs/guide/kimi-k3-quickstart\n * K3 always reasons and supports only `reasoning_effort: \"max\"` — it does NOT\n * use the K2.x binary `thinking: { type }` block, so the wire path routes it\n * through OpenAI-style `reasoning_effort` (see `buildOpenAICompat`).\n */\nconst MOONSHOT_KIMI_K3_COST = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 } as const;\nconst MOONSHOT_KIMI_K3_CONTEXT_WINDOW = 1_048_576;\nconst MOONSHOT_KIMI_K3_MAX_TOKENS = 131_072;\nconst MOONSHOT_KIMI_K3_THINKING: ThinkingConfig = { mode: \"effort\", efforts: [Effort.Max], requiresEffort: true };\n\nexport function moonshotModelManagerOptions(\n\tconfig?: MoonshotModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\t// `MOONSHOT_BASE_URL` redirects discovery (and the streaming request that\n\t// inherits this baseUrl) at the Kimi China platform `api.moonshot.cn`; an\n\t// explicit `config.baseUrl` still wins. Mirrors LITELLM_BASE_URL/LM_STUDIO_BASE_URL. (#2883)\n\tconst baseUrl = config?.baseUrl ?? Bun.env.MOONSHOT_BASE_URL ?? \"https://api.moonshot.ai/v1\";\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"moonshot\");\n\treturn {\n\t\tproviderId: \"moonshot\",\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"moonshot\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\tconst model = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t\tconst id = model.id.toLowerCase();\n\t\t\t\t\t\t// Kimi K3 is discovered but has no bundled/models.dev reference, so the\n\t\t\t\t\t\t// generic dynamic defaults would report it \"Free\" with no capabilities\n\t\t\t\t\t\t// (#5756). Stamp the official pricing/limits when the endpoint doesn't\n\t\t\t\t\t\t// carry them, and mark it reasoning + vision. K3 always reasons via\n\t\t\t\t\t\t// `reasoning_effort: \"max\"` and does NOT use the K2.x `thinking` block,\n\t\t\t\t\t\t// so its thinking config is the single-tier `max` scale — the wire path\n\t\t\t\t\t\t// routes it through `reasoning_effort` (see `buildOpenAICompat`).\n\t\t\t\t\t\tif (!reference && isKimiK3ModelId(id)) {\n\t\t\t\t\t\t\tconst isZeroCost = model.cost.input === 0 && model.cost.output === 0 && model.cost.cacheRead === 0;\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t...model,\n\t\t\t\t\t\t\t\treasoning: true,\n\t\t\t\t\t\t\t\tinput: [\"text\", \"image\"],\n\t\t\t\t\t\t\t\tcost: isZeroCost ? { ...MOONSHOT_KIMI_K3_COST } : model.cost,\n\t\t\t\t\t\t\t\tcontextWindow: model.contextWindow ?? MOONSHOT_KIMI_K3_CONTEXT_WINDOW,\n\t\t\t\t\t\t\t\tmaxTokens: model.maxTokens ?? MOONSHOT_KIMI_K3_MAX_TOKENS,\n\t\t\t\t\t\t\t\tthinking: model.thinking ?? { ...MOONSHOT_KIMI_K3_THINKING },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Moonshot's K2.x family (K2.5, K2.6, kimi-k2-thinking, …) is reasoning-capable\n\t\t\t\t\t\t// and vision-capable on the native API. Without these flags the openai-completions\n\t\t\t\t\t\t// path skips the z.ai-format `thinking` block, and Moonshot K2.6 stalls on first\n\t\t\t\t\t\t// turn because its endpoint expects an explicit `thinking: {type}` (#2113). Match\n\t\t\t\t\t\t// the bundled K2.5 metadata for every K2.x id we discover.\n\t\t\t\t\t\tconst isKimiK2Reasoning = id.includes(\"thinking\") || /(^|\\/)kimi-k2(?:\\.\\d+)?(?:[-:]|$)/.test(id);\n\t\t\t\t\t\tconst isVision =\n\t\t\t\t\t\t\tid.includes(\"vision\") || id.includes(\"vl\") || /(^|\\/)kimi-k2(?:\\.\\d+)?(?:[-:]|$)/.test(id);\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...model,\n\t\t\t\t\t\t\treasoning: isKimiK2Reasoning || model.reasoning,\n\t\t\t\t\t\t\tinput: isVision ? [\"text\", \"image\"] : model.input,\n\t\t\t\t\t\t\tthinking:\n\t\t\t\t\t\t\t\tmodel.thinking ??\n\t\t\t\t\t\t\t\t(isKimiK2Reasoning\n\t\t\t\t\t\t\t\t\t? { mode: \"effort\", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }\n\t\t\t\t\t\t\t\t\t: undefined),\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 16.5 Sakana AI\n// ---------------------------------------------------------------------------\n\nconst SAKANA_DEFAULT_BASE_URL = \"https://api.sakana.ai/v1\";\nconst SAKANA_FREE_ROUTER_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;\nconst SAKANA_FUGU_ULTRA_COST = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 } as const;\nconst SAKANA_FUGU_ULTRA_CONTEXT_WINDOW = 1_000_000;\nconst SAKANA_FUGU_THINKING: ThinkingConfig = {\n\tmode: \"effort\",\n\tefforts: [Effort.High, Effort.Max],\n};\nconst SAKANA_RESPONSES_COMPAT: ModelSpec<\"openai-responses\">[\"compat\"] = {\n\tincludeEncryptedReasoning: false,\n\tstreamIdleTimeoutMs: 0,\n};\n\nfunction normalizeSakanaBaseUrl(baseUrl: string | undefined): string {\n\tconst value = baseUrl?.trim() || SAKANA_DEFAULT_BASE_URL;\n\tconst normalized = value.replace(/\\/+$/, \"\");\n\treturn normalized.endsWith(\"/v1\") ? normalized : `${normalized}/v1`;\n}\n\nfunction isSakanaFuguModelId(modelId: string): boolean {\n\treturn /^fugu(?:$|-)/i.test(modelId);\n}\n\nfunction createSakanaFuguStaticModel(\n\tid: string,\n\tname: string,\n\tcost: ModelSpec<\"openai-responses\">[\"cost\"],\n\tcontextWindow: number | null,\n): ModelSpec<\"openai-responses\"> {\n\treturn {\n\t\tid,\n\t\tname,\n\t\tapi: \"openai-responses\",\n\t\tprovider: \"sakana\",\n\t\tbaseUrl: SAKANA_DEFAULT_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [\"text\"],\n\t\tcost: { ...cost },\n\t\tcontextWindow,\n\t\tmaxTokens: null,\n\t\tthinking: { ...SAKANA_FUGU_THINKING },\n\t\tcompat: { ...SAKANA_RESPONSES_COMPAT },\n\t};\n}\n\nexport const SAKANA_FUGU_STATIC_MODELS: readonly ModelSpec<\"openai-responses\">[] = [\n\tcreateSakanaFuguStaticModel(\"fugu\", \"Fugu\", SAKANA_FREE_ROUTER_COST, SAKANA_FUGU_ULTRA_CONTEXT_WINDOW),\n\tcreateSakanaFuguStaticModel(\"fugu-ultra\", \"Fugu Ultra\", SAKANA_FUGU_ULTRA_COST, SAKANA_FUGU_ULTRA_CONTEXT_WINDOW),\n\tcreateSakanaFuguStaticModel(\n\t\t\"fugu-ultra-20260615\",\n\t\t\"Fugu Ultra 20260615\",\n\t\tSAKANA_FUGU_ULTRA_COST,\n\t\tSAKANA_FUGU_ULTRA_CONTEXT_WINDOW,\n\t),\n];\n\nconst SAKANA_FUGU_STATIC_MODEL_BY_ID = new Map(SAKANA_FUGU_STATIC_MODELS.map(model => [model.id, model] as const));\nconst SAKANA_FUGU_STATIC_MODEL_IDS = SAKANA_FUGU_STATIC_MODELS.map(model => model.id);\n\nexport interface SakanaModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): ModelManagerOptions<\"openai-responses\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = normalizeSakanaBaseUrl(config?.baseUrl ?? Bun.env.SAKANA_BASE_URL ?? Bun.env.FUGU_BASE_URL);\n\tconst references = createBundledReferenceMap<\"openai-responses\">(\"sakana\");\n\treturn {\n\t\tproviderId: \"sakana\",\n\t\tdynamicModelsAuthoritative: true,\n\t\tdropCachedModelIdsOnStaticMismatch: SAKANA_FUGU_STATIC_MODEL_IDS,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-responses\",\n\t\t\t\t\tprovider: \"sakana\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = references.get(defaults.id) ?? SAKANA_FUGU_STATIC_MODEL_BY_ID.get(defaults.id);\n\t\t\t\t\t\tconst model = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t\tif (!reference && isSakanaFuguModelId(model.id)) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t...model,\n\t\t\t\t\t\t\t\treasoning: true,\n\t\t\t\t\t\t\t\tthinking: { ...SAKANA_FUGU_THINKING },\n\t\t\t\t\t\t\t\tcompat: { ...SAKANA_RESPONSES_COMPAT },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn model;\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 16.6 ai& (aiand.com)\n// ---------------------------------------------------------------------------\n\nconst AIAND_DEFAULT_BASE_URL = \"https://api.aiand.com/v1\";\n\n/** `reasoning_efforts` wire values ai& reports, mapped onto pi effort levels. */\nconst AIAND_EFFORT_BY_WIRE_VALUE: Record<string, Effort> = {\n\tminimal: Effort.Minimal,\n\tlow: Effort.Low,\n\tmedium: Effort.Medium,\n\thigh: Effort.High,\n\txhigh: Effort.XHigh,\n\tmax: Effort.Max,\n};\n\nfunction normalizeAiandBaseUrl(baseUrl: string | undefined): string {\n\tconst value = baseUrl?.trim() || AIAND_DEFAULT_BASE_URL;\n\tconst normalized = value.replace(/\\/+$/, \"\");\n\treturn normalized.endsWith(\"/v1\") ? normalized : `${normalized}/v1`;\n}\n\nfunction createAiandStaticModel(\n\tid: string,\n\tname: string,\n\tcost: { input: number; output: number },\n\tcontextWindow: number,\n\tinput: ModelSpec<\"openai-completions\">[\"input\"],\n): ModelSpec<\"openai-completions\"> {\n\treturn {\n\t\tid,\n\t\tname,\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"aiand\",\n\t\tbaseUrl: AIAND_DEFAULT_BASE_URL,\n\t\treasoning: true,\n\t\tinput: [...input],\n\t\tcost: { input: cost.input, output: cost.output, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow,\n\t\tmaxTokens: null,\n\t\tthinking: { mode: \"effort\", efforts: [Effort.Low, Effort.Medium, Effort.High], defaultLevel: Effort.Medium },\n\t};\n}\n\n/**\n * Documented ai& catalog (docs.aiand.com/models/catalog, 2026-08) bundled so\n * the provider is usable when generation and first boot have no live key.\n * The org-scoped `/v1/models` response is authoritative once discovery runs.\n */\nexport const AIAND_STATIC_MODELS: readonly ModelSpec<\"openai-completions\">[] = [\n\tcreateAiandStaticModel(\"qwen/qwen3.6-27b\", \"Qwen3.6 27B\", { input: 0, output: 0 }, 262_144, [\"text\"]),\n\tcreateAiandStaticModel(\n\t\t\"deepseek-ai/deepseek-v4-flash\",\n\t\t\"DeepSeek V4 Flash\",\n\t\t{ input: 0.15, output: 0.25 },\n\t\t1_000_000,\n\t\t[\"text\"],\n\t),\n\tcreateAiandStaticModel(\"google/gemma-4-31b-it\", \"Gemma 4 31B IT\", { input: 0.2, output: 0.5 }, 262_144, [\n\t\t\"text\",\n\t\t\"image\",\n\t]),\n\tcreateAiandStaticModel(\"openai/gpt-oss-120b\", \"GPT OSS 120B\", { input: 0.15, output: 0.6 }, 131_072, [\"text\"]),\n\tcreateAiandStaticModel(\"deepseek-ai/deepseek-v4-pro\", \"DeepSeek V4 Pro\", { input: 1, output: 2.5 }, 1_000_000, [\n\t\t\"text\",\n\t]),\n\tcreateAiandStaticModel(\"moonshotai/kimi-k2.7-code\", \"Kimi K2.7 Code\", { input: 0.75, output: 3.5 }, 262_144, [\n\t\t\"text\",\n\t\t\"image\",\n\t]),\n\tcreateAiandStaticModel(\"moonshotai/kimi-k2.6\", \"Kimi K2.6\", { input: 0.85, output: 3.5 }, 262_144, [\n\t\t\"text\",\n\t\t\"image\",\n\t]),\n\tcreateAiandStaticModel(\"zai-org/glm-5.2\", \"GLM 5.2\", { input: 1, output: 4 }, 1_000_000, [\"text\"]),\n\tcreateAiandStaticModel(\"zai-org/glm-5.1\", \"GLM 5.1\", { input: 1.4, output: 4.4 }, 202_752, [\"text\"]),\n];\n\nconst AIAND_STATIC_MODEL_IDS = AIAND_STATIC_MODELS.map(model => model.id);\n\nfunction mapAiandThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {\n\tconst efforts = Array.isArray(entry.reasoning_efforts)\n\t\t? entry.reasoning_efforts.flatMap(value =>\n\t\t\t\ttypeof value === \"string\" && AIAND_EFFORT_BY_WIRE_VALUE[value] ? [AIAND_EFFORT_BY_WIRE_VALUE[value]] : [],\n\t\t\t)\n\t\t: [];\n\tif (efforts.length === 0) {\n\t\treturn undefined;\n\t}\n\tconst defaultLevel =\n\t\ttypeof entry.reasoning_effort_default === \"string\"\n\t\t\t? AIAND_EFFORT_BY_WIRE_VALUE[entry.reasoning_effort_default]\n\t\t\t: undefined;\n\treturn {\n\t\tmode: \"effort\",\n\t\tefforts,\n\t\t...(defaultLevel && efforts.includes(defaultLevel) && { defaultLevel }),\n\t};\n}\n\n/**\n * ai& reports prices as decimal strings per 1M tokens in the org's billing\n * currency (`usd` or `jpy`). Costs are only mapped for USD orgs — JPY figures\n * would corrupt the USD-denominated cost model, so they fall back to zero.\n */\nfunction mapAiandCost(entry: OpenAICompatibleModelRecord): ModelSpec<\"openai-completions\">[\"cost\"] {\n\tif (typeof entry.currency === \"string\" && entry.currency !== \"usd\") {\n\t\treturn { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\n\t}\n\treturn {\n\t\tinput: toPositiveNumber(entry.input_per_1m, 0),\n\t\toutput: toPositiveNumber(entry.output_per_1m, 0),\n\t\tcacheRead: 0,\n\t\tcacheWrite: 0,\n\t};\n}\n\nfunction mapAiandModel(\n\tentry: OpenAICompatibleModelRecord,\n\tdefaults: ModelSpec<\"openai-completions\">,\n): ModelSpec<\"openai-completions\"> {\n\tconst capabilities: unknown[] = Array.isArray(entry.capabilities) ? entry.capabilities : [];\n\tconst reasoning = capabilities.includes(\"reasoning\");\n\tconst thinking = reasoning ? mapAiandThinking(entry) : undefined;\n\tconst description =\n\t\ttypeof entry.description === \"string\" && entry.description.trim() ? entry.description : undefined;\n\treturn {\n\t\t...defaults,\n\t\tname: description ?? toModelName(entry.name, defaults.name),\n\t\treasoning,\n\t\tinput: capabilities.includes(\"vision\") ? [\"text\", \"image\"] : [\"text\"],\n\t\tcost: mapAiandCost(entry),\n\t\tcontextWindow: toPositiveNumber(entry.context_window, null),\n\t\t...(thinking && { thinking }),\n\t};\n}\n\nexport interface AiandModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\n/**\n * ai& (aiand.com) model manager: OpenAI-compatible chat completions with an\n * org-scoped `/v1/models` catalog carrying context, capability, effort, and\n * pricing metadata, so discovery is authoritative over the bundled seed.\n */\nexport function aiandModelManagerOptions(config?: AiandModelManagerConfig): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = normalizeAiandBaseUrl(config?.baseUrl ?? Bun.env.AIAND_BASE_URL);\n\treturn {\n\t\tproviderId: \"aiand\",\n\t\tdynamicModelsAuthoritative: true,\n\t\tdropCachedModelIdsOnStaticMismatch: AIAND_STATIC_MODEL_IDS,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: () =>\n\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"aiand\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => mapAiandModel(entry, defaults),\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t}),\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 17. Qwen Portal\n// ---------------------------------------------------------------------------\n\nexport interface QwenPortalModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function qwenPortalModelManagerOptions(\n\tconfig?: QwenPortalModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"qwen-portal\", \"https://portal.qwen.ai/v1\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 18. Qianfan\n// ---------------------------------------------------------------------------\n\nexport interface QianfanModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function qianfanModelManagerOptions(\n\tconfig?: QianfanModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\treturn createSimpleOpenAICompletionsOptions(\"qianfan\", \"https://qianfan.baidubce.com/v2\", config);\n}\n\n// ---------------------------------------------------------------------------\n// 19. Cloudflare AI Gateway\n// ---------------------------------------------------------------------------\n\nexport interface CloudflareAiGatewayModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function cloudflareAiGatewayModelManagerOptions(\n\tconfig?: CloudflareAiGatewayModelManagerConfig,\n): ModelManagerOptions<\"anthropic-messages\"> {\n\treturn createSimpleAnthropicProviderOptions(\n\t\t\"cloudflare-ai-gateway\",\n\t\t\"https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic\",\n\t\tconfig,\n\t);\n}\n\n// ---------------------------------------------------------------------------\n// 20. Xiaomi\n// ---------------------------------------------------------------------------\n\n/** Region codes for Xiaomi Token Plan clusters exposed as separate login providers. */\nexport type XiaomiTokenPlanRegion = \"sgp\" | \"ams\" | \"cn\";\n\n/** Configures Xiaomi standard or regional Token Plan OpenAI-compatible model discovery. */\nexport interface XiaomiModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n\tproviderId?: Provider;\n\ttokenPlanRegion?: XiaomiTokenPlanRegion;\n}\n\nconst XIAOMI_TOKEN_PLAN_BASE_URLS: Record<XiaomiTokenPlanRegion, string> = {\n\tsgp: \"https://token-plan-sgp.xiaomimimo.com/v1\",\n\tams: \"https://token-plan-ams.xiaomimimo.com/v1\",\n\tcn: \"https://token-plan-cn.xiaomimimo.com/v1\",\n};\n\nconst XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS = [\n\tXIAOMI_TOKEN_PLAN_BASE_URLS.sgp,\n\tXIAOMI_TOKEN_PLAN_BASE_URLS.ams,\n\tXIAOMI_TOKEN_PLAN_BASE_URLS.cn,\n];\n\n/** Builds a Xiaomi model manager, preserving Token Plan region provider ids during discovery. */\nexport function xiaomiModelManagerOptions(\n\tconfig?: XiaomiModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst providerId = config?.providerId ?? \"xiaomi\";\n\tconst tokenPlanBaseUrls = config?.tokenPlanRegion\n\t\t? [XIAOMI_TOKEN_PLAN_BASE_URLS[config.tokenPlanRegion]]\n\t\t: XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS;\n\tconst XIAOMI_STANDARD_BASE_URL = \"https://api.xiaomimimo.com/v1\";\n\tconst isTokenPlanProvider = config?.tokenPlanRegion !== undefined || providerId.startsWith(\"xiaomi-token-plan-\");\n\tconst isTokenPlanKey = isTokenPlanProvider || apiKey?.startsWith(\"tp-\");\n\t// Token-plan keys always use a TP cluster; config?.baseUrl (from catalog)\n\t// would incorrectly pin to the standard endpoint (api.xiaomimimo.com).\n\tconst baseUrl = isTokenPlanKey ? tokenPlanBaseUrls[0] : (config?.baseUrl ?? XIAOMI_STANDARD_BASE_URL);\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"xiaomi\");\n\tconst fetchModels = (url: string) =>\n\t\tfetchOpenAICompatibleModels({\n\t\t\tapi: \"openai-completions\",\n\t\t\tprovider: providerId,\n\t\t\tbaseUrl: url,\n\t\t\tapiKey,\n\t\t\tfilterModel: (_entry, model) => !model.id.includes(\"-tts\") && !model.id.includes(\"-asr\"),\n\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\tconst model = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\treturn {\n\t\t\t\t\t...model,\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: providerId,\n\t\t\t\t\tbaseUrl: defaults.baseUrl,\n\t\t\t\t\tname: toModelName(entry.display_name, model.name),\n\t\t\t\t};\n\t\t\t},\n\t\t\tfetch: config?.fetch,\n\t\t});\n\treturn {\n\t\tproviderId,\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: async () => {\n\t\t\t\tif (!isTokenPlanKey) {\n\t\t\t\t\treturn fetchModels(baseUrl);\n\t\t\t\t}\n\t\t\t\tfor (const url of tokenPlanBaseUrls) {\n\t\t\t\t\tconst result = await fetchModels(url);\n\t\t\t\t\tif (result) return result;\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t},\n\t\t}),\n\t};\n}\n// ---------------------------------------------------------------------------\n// 21. LiteLLM\n// ---------------------------------------------------------------------------\n\nexport interface LiteLLMModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport interface FetchLiteLLMRichModelsOptions<TApi extends Api> {\n\tapi: TApi;\n\tprovider: Provider;\n\tbaseUrl: string;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tfetch?: FetchImpl;\n\tsignal?: AbortSignal;\n\ttimeoutMs?: number;\n\treferenceResolver?: (modelId: string) => ModelSpec<TApi> | undefined;\n}\n\ntype LiteLLMRichModelEntry = Record<string, unknown>;\ntype LiteLLMRichEndpointModel<TApi extends Api> = {\n\tmodel: ModelSpec<TApi>;\n\tsupportsVision: unknown;\n\tsupportsReasoning: unknown;\n\thasContextWindow: boolean;\n\thasMaxTokens: boolean;\n\thasToolMetadata: boolean;\n\thasSupportedOpenAIParams: boolean;\n\thasCost: boolean;\n};\ntype LiteLLMRichEndpointFailure = {\n\tendpoint: string;\n\treason: \"http-status\" | \"invalid-json\" | \"network-error\";\n\tstatus?: number;\n\terror?: unknown;\n};\ntype LiteLLMRichEndpointResult<TApi extends Api> =\n\t| { models: LiteLLMRichEndpointModel<TApi>[]; incompleteVisionMetadata: boolean }\n\t| { failure: LiteLLMRichEndpointFailure };\n\nconst LITELLM_RICH_ENDPOINTS = [\"/model_group/info\", \"/v2/model/info\", \"/model/info\", \"/v1/model/info\"] as const;\nexport const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128_000;\nexport const OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;\nconst UNKNOWN_PROXY_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;\nconst warnedLiteLLMMetadataBases = new Set<string>();\nconst LITELLM_UNUSABLE_SENTINEL_IDS: Record<string, true> = {\n\t\"all-team-models\": true,\n\t\"all-proxy-models\": true,\n\t\"no-default-models\": true,\n};\nfunction warnLiteLLMMetadataFallback(managementBaseUrl: string, failure: LiteLLMRichEndpointFailure): void {\n\tif (warnedLiteLLMMetadataBases.has(managementBaseUrl)) {\n\t\treturn;\n\t}\n\twarnedLiteLLMMetadataBases.add(managementBaseUrl);\n\tlogger.warn(\"LiteLLM rich model metadata unavailable; falling back to /v1/models\", {\n\t\tendpoint: `${managementBaseUrl}${failure.endpoint}`,\n\t\tstatus: failure.status ?? \"unavailable\",\n\t\treason: failure.reason,\n\t\t...(failure.status === 403\n\t\t\t? { requiredPermission: \"Grant this LiteLLM key access to the model metadata endpoints\" }\n\t\t\t: {}),\n\t\t...(failure.error !== undefined ? { error: failure.error } : {}),\n\t});\n}\n\nexport function normalizeLiteLLMManagementBaseUrl(baseUrl: string): string {\n\tconst trimmed = baseUrl.trim().replace(/\\/+$/g, \"\");\n\tif (!trimmed) {\n\t\treturn \"\";\n\t}\n\ttry {\n\t\tconst parsed = new URL(trimmed);\n\t\tconst path = parsed.pathname.replace(/\\/+$/g, \"\");\n\t\tparsed.pathname = path.endsWith(\"/v1\") ? path.slice(0, -3) || \"/\" : path || \"/\";\n\t\tconst normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n\t\treturn normalized.endsWith(\"/\") ? normalized.slice(0, -1) : normalized;\n\t} catch {\n\t\treturn trimmed.replace(/\\/v1$/, \"\");\n\t}\n}\n\nfunction normalizeLiteLLMRuntimeBaseUrl(baseUrl: string): string {\n\tconst trimmed = baseUrl.trim();\n\treturn trimmed.endsWith(\"/\") ? trimmed.slice(0, -1) : trimmed;\n}\n\nconst LITELLM_RESELLER_USAGE_SUFFIX = /\\s+\\(\\d+(?:\\.\\d+)?[x×] usage\\)$/i;\n\nfunction stripLiteLLMResellerUsageSuffix(name: string): string {\n\tconst cleaned = name.replace(LITELLM_RESELLER_USAGE_SUFFIX, \"\").trim();\n\treturn cleaned.length > 0 ? cleaned : name;\n}\n\nfunction toLiteLLMDisplayName(modelName: string | undefined, referenceName: string | undefined, id: string): string {\n\tconst cleanedModelName = modelName ? stripLiteLLMResellerUsageSuffix(modelName) : undefined;\n\tif (cleanedModelName && cleanedModelName !== id) {\n\t\treturn cleanedModelName;\n\t}\n\treturn referenceName ? stripLiteLLMResellerUsageSuffix(referenceName) : id;\n}\n\nfunction mapLiteLLMOpenAICompatibleModel<TApi extends Api>(\n\tentry: OpenAICompatibleModelRecord,\n\tdefaults: ModelSpec<TApi>,\n\treference: ModelSpec<TApi> | undefined,\n): ModelSpec<TApi> {\n\tconst model = mapWithBundledReference(entry, defaults, reference);\n\treturn {\n\t\t...model,\n\t\tname: stripLiteLLMResellerUsageSuffix(model.name),\n\t};\n}\n\nfunction toNonEmptyString(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tconst trimmed = value.trim();\n\treturn trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction extractLiteLLMRichEntries(payload: unknown): LiteLLMRichModelEntry[] | null {\n\tif (Array.isArray(payload)) {\n\t\treturn payload.flatMap(entry => (isRecord(entry) ? [entry] : []));\n\t}\n\tif (!isRecord(payload)) {\n\t\treturn null;\n\t}\n\tfor (const candidate of [payload.data, payload.models, payload.result, payload.items]) {\n\t\tif (candidate === undefined) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst entries = extractLiteLLMRichEntries(candidate);\n\t\tif (entries !== null) {\n\t\t\treturn entries;\n\t\t}\n\t}\n\treturn null;\n}\n\nfunction getLiteLLMModelInfo(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {\n\treturn isRecord(entry.model_info) ? entry.model_info : undefined;\n}\n\nfunction getLiteLLMParams(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {\n\treturn isRecord(entry.litellm_params) ? entry.litellm_params : undefined;\n}\n\nfunction getLiteLLMMetadataValue(entry: LiteLLMRichModelEntry, key: string): unknown {\n\treturn entry[key] ?? getLiteLLMModelInfo(entry)?.[key];\n}\n\n/** Per-million USD cost from a `*_per_token` LiteLLM field, or `undefined` when absent/non-positive. */\nfunction getLiteLLMPerMillionCost(entry: LiteLLMRichModelEntry, key: string): number | undefined {\n\tconst perToken = toNumber(getLiteLLMMetadataValue(entry, key));\n\treturn perToken !== undefined && perToken > 0 ? perToken * 1_000_000 : undefined;\n}\n\n/**\n * Map LiteLLM's per-token pricing (`input_cost_per_token`, `output_cost_per_token`,\n * cache costs) onto {@link ModelSpec.cost} in $/million tokens. Returns `undefined`\n * when LiteLLM reports neither an input nor an output price so callers keep the\n * bundled reference cost.\n */\nfunction getLiteLLMCost(entry: LiteLLMRichModelEntry): ModelSpec<Api>[\"cost\"] | undefined {\n\tconst input = getLiteLLMPerMillionCost(entry, \"input_cost_per_token\");\n\tconst output = getLiteLLMPerMillionCost(entry, \"output_cost_per_token\");\n\tif (input === undefined && output === undefined) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tinput: input ?? 0,\n\t\toutput: output ?? 0,\n\t\tcacheRead: getLiteLLMPerMillionCost(entry, \"cache_read_input_token_cost\") ?? 0,\n\t\tcacheWrite: getLiteLLMPerMillionCost(entry, \"cache_creation_input_token_cost\") ?? 0,\n\t};\n}\n\nfunction getLiteLLMRichModelId(entry: LiteLLMRichModelEntry): string | undefined {\n\treturn (\n\t\ttoNonEmptyString(entry.model_group) ??\n\t\ttoNonEmptyString(entry.model_name) ??\n\t\ttoNonEmptyString(entry.id) ??\n\t\ttoNonEmptyString(getLiteLLMParams(entry)?.model)\n\t);\n}\n\nfunction getSupportedOpenAIParams(entry: LiteLLMRichModelEntry): string[] | undefined {\n\tconst value = getLiteLLMMetadataValue(entry, \"supported_openai_params\");\n\tif (!Array.isArray(value)) {\n\t\treturn undefined;\n\t}\n\treturn value.flatMap(item => (typeof item === \"string\" ? [item] : []));\n}\n\nfunction isLiteLLMUnusableSentinelPlaceholder(entry: LiteLLMRichModelEntry): boolean {\n\tconst modelGroup = toNonEmptyString(entry.model_group);\n\tconst id = toNonEmptyString(entry.id);\n\tif (\n\t\t(modelGroup === undefined || LITELLM_UNUSABLE_SENTINEL_IDS[modelGroup] !== true) &&\n\t\t(id === undefined || LITELLM_UNUSABLE_SENTINEL_IDS[id] !== true)\n\t) {\n\t\treturn false;\n\t}\n\tconst providers = entry.providers;\n\tif (providers !== undefined && (!Array.isArray(providers) || providers.length > 0)) {\n\t\treturn false;\n\t}\n\tconst modelName = toNonEmptyString(entry.model_name);\n\tif (modelName && LITELLM_UNUSABLE_SENTINEL_IDS[modelName] !== true) {\n\t\treturn false;\n\t}\n\tif (id && LITELLM_UNUSABLE_SENTINEL_IDS[id] !== true) {\n\t\treturn false;\n\t}\n\tconst backendModel = toNonEmptyString(getLiteLLMParams(entry)?.model);\n\tif (backendModel && LITELLM_UNUSABLE_SENTINEL_IDS[backendModel] !== true) {\n\t\treturn false;\n\t}\n\tif (\n\t\ttoPositiveNumber(getLiteLLMMetadataValue(entry, \"max_input_tokens\"), null) !== null ||\n\t\ttoPositiveNumber(getLiteLLMMetadataValue(entry, \"max_output_tokens\"), null) !== null\n\t) {\n\t\treturn false;\n\t}\n\tif (\n\t\tgetLiteLLMMetadataValue(entry, \"supports_vision\") === true ||\n\t\tgetLiteLLMMetadataValue(entry, \"supports_reasoning\") === true ||\n\t\tgetLiteLLMMetadataValue(entry, \"supports_function_calling\") === true ||\n\t\tgetLiteLLMMetadataValue(entry, \"supports_tools\") === true\n\t) {\n\t\treturn false;\n\t}\n\tconst supportedOpenAIParams = getSupportedOpenAIParams(entry);\n\tif (supportedOpenAIParams && supportedOpenAIParams.length > 0) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction mapLiteLLMRichEntry<TApi extends Api>(\n\tentry: LiteLLMRichModelEntry,\n\toptions: FetchLiteLLMRichModelsOptions<TApi>,\n\truntimeBaseUrl: string,\n): ModelSpec<TApi> | null {\n\tif (isLiteLLMUnusableSentinelPlaceholder(entry)) {\n\t\treturn null;\n\t}\n\tconst id = getLiteLLMRichModelId(entry);\n\tif (!id) {\n\t\treturn null;\n\t}\n\tconst reference = options.referenceResolver?.(id);\n\tconst modelName = toNonEmptyString(entry.model_name);\n\tconst contextWindow = toPositiveNumber(\n\t\tgetLiteLLMMetadataValue(entry, \"max_input_tokens\"),\n\t\treference?.contextWindow ?? OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW,\n\t);\n\tconst maxTokens = toPositiveNumber(\n\t\tgetLiteLLMMetadataValue(entry, \"max_output_tokens\"),\n\t\treference?.maxTokens ?? Math.min(contextWindow, OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS),\n\t);\n\tconst supportsVision = getLiteLLMMetadataValue(entry, \"supports_vision\");\n\tconst supportsReasoning = getLiteLLMMetadataValue(entry, \"supports_reasoning\");\n\tconst supportedOpenAIParams = getSupportedOpenAIParams(entry);\n\tconst supportsFunctionCalling = getLiteLLMMetadataValue(entry, \"supports_function_calling\");\n\tconst supportsTools =\n\t\tsupportsFunctionCalling === true\n\t\t\t? true\n\t\t\t: supportsFunctionCalling === false\n\t\t\t\t? false\n\t\t\t\t: supportedOpenAIParams !== undefined\n\t\t\t\t\t? supportedOpenAIParams.some(param =>\n\t\t\t\t\t\t\t[\"tools\", \"tool_choice\", \"functions\", \"function_call\"].includes(param),\n\t\t\t\t\t\t)\n\t\t\t\t\t: reference?.supportsTools;\n\tconst compat: OpenAICompat = {\n\t\t...(reference?.compat ?? {}),\n\t\tsupportsStore: false,\n\t\tsupportsDeveloperRole: false,\n\t\t...(supportedOpenAIParams !== undefined\n\t\t\t? { supportsReasoningEffort: supportedOpenAIParams.includes(\"reasoning_effort\") }\n\t\t\t: {}),\n\t};\n\treturn {\n\t\tid,\n\t\tname: toLiteLLMDisplayName(modelName, reference?.name, id),\n\t\tapi: options.api,\n\t\tprovider: options.provider,\n\t\tbaseUrl: runtimeBaseUrl,\n\t\tcontextWindow,\n\t\tmaxTokens,\n\t\tinput:\n\t\t\tsupportsVision === true\n\t\t\t\t? [\"text\", \"image\"]\n\t\t\t\t: supportsVision === false\n\t\t\t\t\t? [\"text\"]\n\t\t\t\t\t: (reference?.input ?? [\"text\"]),\n\t\treasoning: typeof supportsReasoning === \"boolean\" ? supportsReasoning : (reference?.reasoning ?? false),\n\t\tthinking: reference?.thinking,\n\t\tcost: getLiteLLMCost(entry) ?? reference?.cost ?? UNKNOWN_PROXY_COST,\n\t\t...(supportsTools !== undefined ? { supportsTools } : {}),\n\t\tcompat: compat as ModelSpec<TApi>[\"compat\"],\n\t};\n}\n\nasync function fetchLiteLLMRichEndpoint<TApi extends Api>(\n\tendpoint: string,\n\toptions: FetchLiteLLMRichModelsOptions<TApi>,\n\tmanagementBaseUrl: string,\n\truntimeBaseUrl: string,\n\tsignal?: AbortSignal,\n): Promise<LiteLLMRichEndpointResult<TApi> | null> {\n\tconst fetchImpl = discoveryFetch(options.fetch);\n\tconst requestHeaders: Record<string, string> = {\n\t\tAccept: \"application/json\",\n\t\t...options.headers,\n\t};\n\tif (options.apiKey) {\n\t\trequestHeaders.Authorization = `Bearer ${options.apiKey}`;\n\t}\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetchImpl(`${managementBaseUrl}${endpoint}`, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: requestHeaders,\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\treturn { failure: { endpoint, reason: \"network-error\", error } };\n\t}\n\tif (!response.ok) {\n\t\treturn response.status === 404 ? null : { failure: { endpoint, reason: \"http-status\", status: response.status } };\n\t}\n\tlet payload: unknown;\n\ttry {\n\t\tpayload = await response.json();\n\t} catch (error) {\n\t\treturn { failure: { endpoint, reason: \"invalid-json\", status: response.status, error } };\n\t}\n\tconst entries = extractLiteLLMRichEntries(payload);\n\tif (!entries || entries.length === 0) {\n\t\treturn null;\n\t}\n\tconst deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();\n\tlet incompleteVisionMetadata = false;\n\tfor (const entry of entries) {\n\t\tconst model = mapLiteLLMRichEntry(entry, options, runtimeBaseUrl);\n\t\tif (model) {\n\t\t\tconst supportsVision = getLiteLLMMetadataValue(entry, \"supports_vision\");\n\t\t\tconst supportsReasoning = getLiteLLMMetadataValue(entry, \"supports_reasoning\");\n\t\t\tconst supportsFunctionCalling = getLiteLLMMetadataValue(entry, \"supports_function_calling\");\n\t\t\tconst supportedOpenAIParams = getSupportedOpenAIParams(entry);\n\t\t\tif (supportsVision !== true && supportsVision !== false) {\n\t\t\t\tincompleteVisionMetadata = true;\n\t\t\t}\n\t\t\tdeduped.set(model.id, {\n\t\t\t\tmodel,\n\t\t\t\tsupportsVision,\n\t\t\t\tsupportsReasoning,\n\t\t\t\thasContextWindow: toPositiveNumber(getLiteLLMMetadataValue(entry, \"max_input_tokens\"), null) !== null,\n\t\t\t\thasMaxTokens: toPositiveNumber(getLiteLLMMetadataValue(entry, \"max_output_tokens\"), null) !== null,\n\t\t\t\thasToolMetadata:\n\t\t\t\t\tsupportsFunctionCalling === true ||\n\t\t\t\t\tsupportsFunctionCalling === false ||\n\t\t\t\t\tsupportedOpenAIParams !== undefined,\n\t\t\t\thasSupportedOpenAIParams: supportedOpenAIParams !== undefined,\n\t\t\t\thasCost: getLiteLLMCost(entry) !== undefined,\n\t\t\t});\n\t\t}\n\t}\n\tif (deduped.size === 0) {\n\t\treturn null;\n\t}\n\treturn {\n\t\tmodels: Array.from(deduped.values()).sort((left, right) => left.model.id.localeCompare(right.model.id)),\n\t\tincompleteVisionMetadata,\n\t};\n}\n\nexport async function fetchLiteLLMRichModels<TApi extends Api>(\n\toptions: FetchLiteLLMRichModelsOptions<TApi>,\n): Promise<ModelSpec<TApi>[] | null> {\n\tconst managementBaseUrl = normalizeLiteLLMManagementBaseUrl(options.baseUrl);\n\tconst runtimeBaseUrl = normalizeLiteLLMRuntimeBaseUrl(options.baseUrl);\n\tif (!managementBaseUrl || !runtimeBaseUrl) {\n\t\treturn null;\n\t}\n\tconst fetchModels = async (signal?: AbortSignal): Promise<ModelSpec<TApi>[] | null> => {\n\t\tconst deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();\n\t\tlet metadataFailure: LiteLLMRichEndpointFailure | undefined;\n\t\tfor (const endpoint of LITELLM_RICH_ENDPOINTS) {\n\t\t\tconst result = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl, signal);\n\t\t\tif (!result) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (\"failure\" in result) {\n\t\t\t\t// A 401 is a retryable auth failure owned by the caller's auth-retry\n\t\t\t\t// path (withAuth refresh/sibling rotation in discoverLiteLLMModels);\n\t\t\t\t// recording it here would log a fallback warning on a stale first\n\t\t\t\t// credential before the refreshed retry ultimately serves rich\n\t\t\t\t// metadata. Forbidden (403) and other failures are never retried, so\n\t\t\t\t// they remain warn-worthy and keep priority.\n\t\t\t\tif (\n\t\t\t\t\tresult.failure.status !== 401 &&\n\t\t\t\t\t(!metadataFailure || (metadataFailure.status !== 403 && result.failure.status === 403))\n\t\t\t\t) {\n\t\t\t\t\tmetadataFailure = result.failure;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst hadPriorModels = deduped.size > 0;\n\t\t\tfor (const next of result.models) {\n\t\t\t\tconst existing = deduped.get(next.model.id);\n\t\t\t\tif (!existing) {\n\t\t\t\t\tif (!hadPriorModels) {\n\t\t\t\t\t\tdeduped.set(next.model.id, next);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst model: ModelSpec<TApi> = {\n\t\t\t\t\t...existing.model,\n\t\t\t\t\tname: next.model.name === next.model.id ? existing.model.name : next.model.name,\n\t\t\t\t\tcontextWindow: next.hasContextWindow ? next.model.contextWindow : existing.model.contextWindow,\n\t\t\t\t\tmaxTokens: next.hasMaxTokens ? next.model.maxTokens : existing.model.maxTokens,\n\t\t\t\t\tinput:\n\t\t\t\t\t\tnext.supportsVision === true || next.supportsVision === false\n\t\t\t\t\t\t\t? next.model.input\n\t\t\t\t\t\t\t: existing.model.input,\n\t\t\t\t\treasoning: typeof next.supportsReasoning === \"boolean\" ? next.model.reasoning : existing.model.reasoning,\n\t\t\t\t\tcost: next.hasCost ? next.model.cost : existing.model.cost,\n\t\t\t\t\tcompat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,\n\t\t\t\t};\n\t\t\t\tif (next.hasToolMetadata) {\n\t\t\t\t\tmodel.supportsTools = next.model.supportsTools;\n\t\t\t\t}\n\t\t\t\tdeduped.set(next.model.id, { ...next, model });\n\t\t\t}\n\t\t\tlet hasIncompleteVisionMetadata = false;\n\t\t\tfor (const entry of deduped.values()) {\n\t\t\t\tif (entry.supportsVision !== true && entry.supportsVision !== false) {\n\t\t\t\t\thasIncompleteVisionMetadata = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!hasIncompleteVisionMetadata) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (deduped.size === 0) {\n\t\t\tif (metadataFailure) {\n\t\t\t\twarnLiteLLMMetadataFallback(managementBaseUrl, metadataFailure);\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\treturn Array.from(deduped.values())\n\t\t\t.map(entry => entry.model)\n\t\t\t.sort((left, right) => left.id.localeCompare(right.id));\n\t};\n\tif (options.signal !== undefined) {\n\t\treturn fetchModels(options.signal);\n\t}\n\treturn options.timeoutMs !== undefined ? withCatalogDiscoveryTimeout(options.timeoutMs, fetchModels) : fetchModels();\n}\n\nexport function litellmModelManagerOptions(\n\tconfig?: LiteLLMModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl(\"litellm\")!;\n\treturn {\n\t\tproviderId: \"litellm\",\n\t\t// rich-v5 invalidates rows cached before rich metadata pricing was mapped.\n\t\t// Earlier versions added bundled reference fallback, continued discovery\n\t\t// past incomplete `/model_group/info`, stripped reseller usage suffixes,\n\t\t// and filtered placeholder-only `all-team-models` rows. Bump the version\n\t\t// whenever the mappers below change, or warm authoritative caches keep\n\t\t// serving pre-change rows for the full TTL.\n\t\tcacheProviderId: resolveModelCacheProviderId(\"litellm\", { baseUrl }),\n\t\t// litellm is a local-only proxy and is never bundled in models.json (that\n\t\t// would leak the machine's localhost catalog). Prefer the proxy's richer\n\t\t// management metadata, then enrich ids against models.dev with the bundled\n\t\t// catalog as a fallback before using /v1/models.\n\t\tfetchDynamicModels: async () => {\n\t\t\tconst modelsDevReferences = await loadModelsDevReferences<\"openai-completions\">(config?.fetch);\n\t\t\tconst resolveReference = createReferenceResolver(modelsDevReferences);\n\t\t\tconst richModels = await fetchLiteLLMRichModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"litellm\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tfetch: config?.fetch,\n\t\t\t\treferenceResolver: resolveReference,\n\t\t\t\ttimeoutMs: 10_000,\n\t\t\t});\n\t\t\tif (richModels && richModels.length > 0) {\n\t\t\t\treturn richModels;\n\t\t\t}\n\t\t\treturn fetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"litellm\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) =>\n\t\t\t\t\tmapLiteLLMOpenAICompatibleModel(entry, defaults, resolveReference(defaults.id)),\n\t\t\t\tfetch: config?.fetch,\n\t\t\t});\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 22. vLLM\n// ---------------------------------------------------------------------------\n\nconst VLLM_DISCOVERY_TIMEOUT_MS = 10_000;\n\nexport interface VllmModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl(\"vllm\")!;\n\tconst references = createBundledReferenceMap<\"openai-completions\">(\"vllm\" as Parameters<typeof getBundledModels>[0]);\n\treturn {\n\t\tproviderId: \"vllm\",\n\t\tcacheProviderId: resolveModelCacheProviderId(\"vllm\", { baseUrl }),\n\t\tfetchDynamicModels: () =>\n\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"vllm\",\n\t\t\t\tbaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\tconst model = mapWithBundledReference(entry, defaults, references.get(defaults.id));\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...model,\n\t\t\t\t\t\tcontextWindow: toPositiveNumber(entry.max_model_len, model.contextWindow),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tfetch: config?.fetch,\n\t\t\t\ttimeoutMs: VLLM_DISCOVERY_TIMEOUT_MS,\n\t\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 23. NanoGPT\n// ---------------------------------------------------------------------------\n\nexport interface NanoGptModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function nanoGptModelManagerOptions(\n\tconfig?: NanoGptModelManagerConfig,\n): ModelManagerOptions<\"openai-completions\"> {\n\tconst apiKey = config?.apiKey;\n\tconst baseUrl = config?.baseUrl ?? \"https://nano-gpt.com/api/v1\";\n\tconst resolveReference = createReferenceResolver(() =>\n\t\tcreateBundledReferenceMap<\"openai-completions\">(\"nanogpt\" as Parameters<typeof getBundledModels>[0]),\n\t);\n\treturn {\n\t\tproviderId: \"nanogpt\",\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: async () => {\n\t\t\t\t// Track base IDs that have :thinking variants so we can mark them reasoning-capable.\n\t\t\t\tconst thinkingBaseIds = new Set<string>();\n\t\t\t\tconst models = await fetchOpenAICompatibleModels({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"nanogpt\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\tmapModel: (entry, defaults) => {\n\t\t\t\t\t\tconst reference = resolveReference(defaults.id);\n\t\t\t\t\t\tconst mapped = mapWithBundledReference(entry, defaults, reference);\n\t\t\t\t\t\treturn { ...mapped, api: \"openai-completions\", provider: \"nanogpt\" };\n\t\t\t\t\t},\n\t\t\t\t\tfilterModel: (_entry, model) => {\n\t\t\t\t\t\tconst match = NANO_GPT_THINKING_SUFFIX_RE.exec(model.id);\n\t\t\t\t\t\tif (match) {\n\t\t\t\t\t\t\tthinkingBaseIds.add(model.id.slice(0, match.index));\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn isLikelyNanoGptTextModelId(model.id);\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t});\n\t\t\t\tif (!models) return null;\n\t\t\t\t// Mark base models as reasoning-capable when a :thinking variant existed.\n\t\t\t\tfor (const model of models) {\n\t\t\t\t\tif (!model.reasoning && thinkingBaseIds.has(model.id)) {\n\t\t\t\t\t\t(model as { reasoning: boolean }).reasoning = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn models;\n\t\t\t},\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 24. GitHub Copilot\n// ---------------------------------------------------------------------------\n\nexport interface GithubCopilotModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nconst COPILOT_ANTHROPIC_MODEL_PATTERN = /^claude-(haiku|sonnet|opus|fable|mythos)-\\d/;\nconst isCopilotResponsesModelId = (modelId: string): boolean =>\n\tmodelId === \"grok-4.5\" || modelId.startsWith(\"gpt-5\") || modelId.startsWith(\"oswe\") || modelId.startsWith(\"mai-\");\nconst COPILOT_CACHE_INVALIDATED_MODEL_IDS = [\"grok-4.5\", \"grok-4.5-1m\", \"mai-code-1-flash-picker\"];\n\nfunction inferCopilotApi(modelId: string): Api {\n\tif (COPILOT_ANTHROPIC_MODEL_PATTERN.test(modelId)) {\n\t\treturn \"anthropic-messages\";\n\t}\n\tif (isCopilotResponsesModelId(modelId)) {\n\t\treturn \"openai-responses\";\n\t}\n\treturn \"openai-completions\";\n}\n\nfunction extractCopilotLimits(entry: OpenAICompatibleModelRecord): {\n\tmaxPromptTokens?: number;\n\tmaxContextWindowTokens?: number;\n\tmaxOutputTokens?: number;\n\tmaxNonStreamingOutputTokens?: number;\n} {\n\tif (!isRecord(entry.capabilities)) {\n\t\treturn {};\n\t}\n\tconst limitsValue = entry.capabilities.limits;\n\tif (!isRecord(limitsValue)) {\n\t\treturn {};\n\t}\n\treturn {\n\t\tmaxPromptTokens: toNumber(limitsValue.max_prompt_tokens),\n\t\tmaxContextWindowTokens: toNumber(limitsValue.max_context_window_tokens),\n\t\tmaxOutputTokens: toNumber(limitsValue.max_output_tokens),\n\t\tmaxNonStreamingOutputTokens: toNumber(limitsValue.max_non_streaming_output_tokens),\n\t};\n}\n\n/** Local id/name suffixes for synthesized Copilot long-context variants. */\nexport const COPILOT_LONG_CONTEXT_ID_SUFFIX = \"-1m\";\nconst COPILOT_LONG_CONTEXT_NAME_SUFFIX = \" (1M)\";\n\n/** One tier of Copilot token pricing (`billing.token_prices.{default,long_context}`). Prices are hundredths of a dollar per 1M tokens. */\ninterface CopilotTokenPriceTier {\n\tcontextMax?: number;\n\tinputPrice?: number;\n\toutputPrice?: number;\n\tcachePrice?: number;\n}\n\nfunction parseCopilotTokenPriceTier(value: unknown): CopilotTokenPriceTier | undefined {\n\tif (!isRecord(value)) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tcontextMax: toNumber(value.context_max),\n\t\tinputPrice: toNumber(value.input_price),\n\t\toutputPrice: toNumber(value.output_price),\n\t\tcachePrice: toNumber(value.cache_price),\n\t};\n}\n\n/**\n * Tiered context boundaries/prices from `billing.token_prices`. Served only\n * when discovery requests `X-GitHub-Api-Version` ≥ 2026-06-01; absent on the\n * legacy response shape (where `capabilities.limits` is already tier-capped).\n */\nfunction extractCopilotTokenPrices(entry: OpenAICompatibleModelRecord): {\n\tdefaultTier?: CopilotTokenPriceTier;\n\tlongContext?: CopilotTokenPriceTier;\n} {\n\tif (!isRecord(entry.billing)) {\n\t\treturn {};\n\t}\n\tconst tokenPrices = entry.billing.token_prices;\n\tif (!isRecord(tokenPrices)) {\n\t\treturn {};\n\t}\n\treturn {\n\t\tdefaultTier: parseCopilotTokenPriceTier(tokenPrices.default),\n\t\tlongContext: parseCopilotTokenPriceTier(tokenPrices.long_context),\n\t};\n}\n\nfunction extractCopilotSupportsVision(entry: OpenAICompatibleModelRecord): boolean | undefined {\n\tif (!isRecord(entry.capabilities)) {\n\t\treturn undefined;\n\t}\n\tconst supports = entry.capabilities.supports;\n\tif (!isRecord(supports)) {\n\t\treturn undefined;\n\t}\n\treturn toBoolean(supports.vision);\n}\n\n/** Copilot's `/models` mixes chat and embedding models; only `type: \"chat\"` entries are usable here. */\nfunction isCopilotChatModel(entry: OpenAICompatibleModelRecord): boolean {\n\tif (!isRecord(entry.capabilities)) {\n\t\treturn true;\n\t}\n\tconst type = entry.capabilities.type;\n\treturn typeof type !== \"string\" || type === \"chat\";\n}\n\nfunction copilotTierCost(\n\ttier: CopilotTokenPriceTier | undefined,\n): Omit<ModelSpec<Api>[\"cost\"], \"cacheWrite\"> | undefined {\n\tif (tier?.inputPrice === undefined || tier.outputPrice === undefined) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tinput: tier.inputPrice / 100,\n\t\toutput: tier.outputPrice / 100,\n\t\tcacheRead: (tier.cachePrice ?? 0) / 100,\n\t};\n}\n\n/**\n * Synthesize the opt-in long-context sibling for a Copilot model that reports\n * a `billing.token_prices.long_context` tier (e.g. Claude Opus 200k → 1M, as\n * selectable in copilot-cli). The variant is a local catalog entry: it keeps\n * the upstream model id on the wire via `requestModelId` — the tier is purely\n * a client-side context budget with its own pricing, not a served model id.\n * The base entry stays on the default tier so nobody silently pays\n * long-context rates.\n */\nfunction createCopilotLongContextVariant(\n\tbase: ModelSpec<Api>,\n\tfullContextWindow: number | null,\n\tmaxTokens: number | null,\n\tlongContext: CopilotTokenPriceTier | undefined,\n): ModelSpec<Api> | undefined {\n\tconst longContextMax = longContext?.contextMax;\n\tif (longContextMax === undefined || longContextMax <= 0 || fullContextWindow === null || maxTokens === null) {\n\t\treturn undefined;\n\t}\n\tconst variantWindow = Math.min(fullContextWindow, longContextMax + maxTokens);\n\tif (base.contextWindow === null || variantWindow <= base.contextWindow) {\n\t\treturn undefined;\n\t}\n\tconst longCost = copilotTierCost(longContext);\n\treturn {\n\t\t...base,\n\t\tid: `${base.id}${COPILOT_LONG_CONTEXT_ID_SUFFIX}`,\n\t\trequestModelId: base.id,\n\t\tname: `${base.name}${COPILOT_LONG_CONTEXT_NAME_SUFFIX}`,\n\t\tcontextWindow: variantWindow,\n\t\t// Long-context tier has its own token prices (Gemini/GPT bill ~2x above\n\t\t// the default boundary). cacheWrite is not reported per tier; inherit.\n\t\t...(longCost && { cost: { ...longCost, cacheWrite: base.cost.cacheWrite } }),\n\t\tcontextPromotionTarget: undefined,\n\t};\n}\n\nexport function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api> {\n\tconst rawApiKey = config?.apiKey;\n\tconst configuredBaseUrl = config?.baseUrl ?? \"https://api.githubcopilot.com\";\n\tconst parsedApiKey = rawApiKey ? parseGitHubCopilotApiKey(rawApiKey) : undefined;\n\tconst apiKey = parsedApiKey?.accessToken;\n\tconst baseUrl =\n\t\tparsedApiKey?.apiEndpoint && configuredBaseUrl.includes(\"githubcopilot.com\")\n\t\t\t? parsedApiKey.apiEndpoint\n\t\t\t: parsedApiKey?.enterpriseUrl && configuredBaseUrl.includes(\"githubcopilot.com\")\n\t\t\t\t? getGitHubCopilotBaseUrl(parsedApiKey.enterpriseUrl)\n\t\t\t\t: configuredBaseUrl;\n\tlet providerReferences: Map<string, ModelSpec<Api>> | undefined;\n\tconst getProviderReferences = () => (providerReferences ??= createBundledReferenceMap<Api>(\"github-copilot\"));\n\tconst resolveReference = createReferenceResolver(getProviderReferences);\n\treturn {\n\t\tproviderId: \"github-copilot\",\n\t\tdropCachedModelIdsOnStaticMismatch: COPILOT_CACHE_INVALIDATED_MODEL_IDS,\n\t\t// COPILOT_API_HEADERS are compile-time constants (User-Agent + API\n\t\t// version), not credentials. The cache omits all request headers for\n\t\t// safety and can only restore them from a bundled static entry — so a\n\t\t// Copilot model with no bundled reference (e.g. a freshly served\n\t\t// claude-opus-5 and its synthesized -1m sibling) is dropped on offline\n\t\t// reads. Declaring the constant lets the cache restore it by value.\n\t\trestorableHeaderFallback: { ...COPILOT_API_HEADERS },\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: async () => {\n\t\t\t\tconst longContextVariants: ModelSpec<Api>[] = [];\n\t\t\t\tconst models = await fetchOpenAICompatibleModels<Api>({\n\t\t\t\t\tapi: \"openai-completions\",\n\t\t\t\t\tprovider: \"github-copilot\",\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders: COPILOT_API_HEADERS,\n\t\t\t\t\tmapModel: (\n\t\t\t\t\t\tentry: OpenAICompatibleModelRecord,\n\t\t\t\t\t\tdefaults: ModelSpec<Api>,\n\t\t\t\t\t\t_context: OpenAICompatibleModelMapperContext<Api>,\n\t\t\t\t\t): ModelSpec<Api> | null => {\n\t\t\t\t\t\tif (!isCopilotChatModel(entry)) {\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst reference = resolveReference(defaults.id);\n\t\t\t\t\t\tconst copilotLimits = extractCopilotLimits(entry);\n\t\t\t\t\t\t// Copilot exposes token limits under capabilities.limits.*.\n\t\t\t\t\t\t// max_context_window_tokens is the model's total usable window;\n\t\t\t\t\t\t// max_prompt_tokens is Copilot's prompt/summarization budget and\n\t\t\t\t\t\t// must only be a fallback when total-window fields are absent.\n\t\t\t\t\t\tconst contextWindow = toPositiveNumber(\n\t\t\t\t\t\t\tcopilotLimits.maxContextWindowTokens,\n\t\t\t\t\t\t\ttoPositiveNumber(\n\t\t\t\t\t\t\t\tentry.context_length,\n\t\t\t\t\t\t\t\ttoPositiveNumber(\n\t\t\t\t\t\t\t\t\tcopilotLimits.maxPromptTokens,\n\t\t\t\t\t\t\t\t\treference?.contextWindow ?? defaults.contextWindow,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst maxTokens = toPositiveNumber(\n\t\t\t\t\t\t\tcopilotLimits.maxOutputTokens,\n\t\t\t\t\t\t\ttoPositiveNumber(\n\t\t\t\t\t\t\t\tentry.max_completion_tokens,\n\t\t\t\t\t\t\t\ttoPositiveNumber(\n\t\t\t\t\t\t\t\t\tcopilotLimits.maxNonStreamingOutputTokens,\n\t\t\t\t\t\t\t\t\treference?.maxTokens ?? defaults.maxTokens,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst name =\n\t\t\t\t\t\t\ttypeof entry.name === \"string\" && entry.name.trim().length > 0\n\t\t\t\t\t\t\t\t? entry.name\n\t\t\t\t\t\t\t\t: (reference?.name ?? defaults.name);\n\t\t\t\t\t\tconst api = inferCopilotApi(defaults.id);\n\t\t\t\t\t\tconst supportsVision = extractCopilotSupportsVision(entry);\n\t\t\t\t\t\tconst input: ModelSpec<Api>[\"input\"] =\n\t\t\t\t\t\t\tsupportsVision === true\n\t\t\t\t\t\t\t\t? [\"text\", \"image\"]\n\t\t\t\t\t\t\t\t: supportsVision === false || !isPersonalGitHubCopilotBaseUrl(baseUrl)\n\t\t\t\t\t\t\t\t\t? [\"text\"]\n\t\t\t\t\t\t\t\t\t: (reference?.input ?? defaults.input);\n\t\t\t\t\t\t// With COPILOT_API_HEADERS the served window is the long-context\n\t\t\t\t\t\t// ceiling; the default tier ends at token_prices.default.context_max\n\t\t\t\t\t\t// prompt tokens. Cap the base entry to the default tier — the long\n\t\t\t\t\t\t// tier is the opt-in `-1m` sibling below.\n\t\t\t\t\t\tconst tokenPrices = extractCopilotTokenPrices(entry);\n\t\t\t\t\t\tconst defaultContextMax = tokenPrices.defaultTier?.contextMax;\n\t\t\t\t\t\tconst defaultTierWindow =\n\t\t\t\t\t\t\tdefaultContextMax !== undefined &&\n\t\t\t\t\t\t\tdefaultContextMax > 0 &&\n\t\t\t\t\t\t\tcontextWindow !== null &&\n\t\t\t\t\t\t\tmaxTokens !== null\n\t\t\t\t\t\t\t\t? Math.min(contextWindow, defaultContextMax + maxTokens)\n\t\t\t\t\t\t\t\t: contextWindow;\n\t\t\t\t\t\tconst base: ModelSpec<Api> = reference\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t...reference,\n\t\t\t\t\t\t\t\t\tapi,\n\t\t\t\t\t\t\t\t\tprovider: \"github-copilot\",\n\t\t\t\t\t\t\t\t\tbaseUrl,\n\t\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\tcontextWindow: defaultTierWindow,\n\t\t\t\t\t\t\t\t\tmaxTokens,\n\t\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t\t...COPILOT_API_HEADERS,\n\t\t\t\t\t\t\t\t\t\t...(getProviderReferences().get(defaults.id)?.headers ?? {}),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t...(api === \"openai-completions\"\n\t\t\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\t\t\tcompat: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsupportsStore: false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tsupportsReasoningEffort: false,\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\t\t\t\tapi,\n\t\t\t\t\t\t\t\t\tbaseUrl,\n\t\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\tcontextWindow: defaultTierWindow,\n\t\t\t\t\t\t\t\t\tmaxTokens,\n\t\t\t\t\t\t\t\t\theaders: { ...COPILOT_API_HEADERS },\n\t\t\t\t\t\t\t\t\t// Copilot's `/models` advertises no reasoning bit, so a\n\t\t\t\t\t\t\t\t\t// thinking-capable Claude with no bundled reference would\n\t\t\t\t\t\t\t\t\t// fall back to `reasoning: false` and lose its effort dial.\n\t\t\t\t\t\t\t\t\t// Gate on the id classifier (not the transport alone) so a\n\t\t\t\t\t\t\t\t\t// lagging enterprise catalog serving a pre-thinking Claude\n\t\t\t\t\t\t\t\t\t// (<= 3.5) over the Messages proxy is not handed a fabricated\n\t\t\t\t\t\t\t\t\t// dial it would reject; a modern reference-less model (e.g.\n\t\t\t\t\t\t\t\t\t// claude-opus-5) is marked so `buildModel` derives the ladder.\n\t\t\t\t\t\t\t\t\t...(api === \"anthropic-messages\" && anthropicModelSupportsThinking(defaults.id)\n\t\t\t\t\t\t\t\t\t\t? { reasoning: true }\n\t\t\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t\t\t\t...(api === \"openai-completions\"\n\t\t\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\t\t\tcompat: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsupportsStore: false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tsupportsReasoningEffort: false,\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\tconst variant = createCopilotLongContextVariant(\n\t\t\t\t\t\t\tbase,\n\t\t\t\t\t\t\tcontextWindow,\n\t\t\t\t\t\t\tmaxTokens,\n\t\t\t\t\t\t\ttokenPrices.longContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (variant) {\n\t\t\t\t\t\t\tlongContextVariants.push(variant);\n\t\t\t\t\t\t\t// Overflowing the default tier promotes into the 1M sibling\n\t\t\t\t\t\t\t// unless the reference already pins a target.\n\t\t\t\t\t\t\tbase.contextPromotionTarget ??= `github-copilot/${variant.id}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn base;\n\t\t\t\t\t},\n\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t});\n\t\t\t\tif (models === null) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\t// Append synthesized tiers; a real upstream id always wins over a\n\t\t\t\t// local variant with the same id.\n\t\t\t\tconst takenIds = new Set(models.map(model => model.id));\n\t\t\t\tfor (const variant of longContextVariants) {\n\t\t\t\t\tif (takenIds.has(variant.id)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\ttakenIds.add(variant.id);\n\t\t\t\t\tmodels.push(variant);\n\t\t\t\t}\n\t\t\t\treturn models.sort((left, right) => left.id.localeCompare(right.id));\n\t\t\t},\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// 24. Anthropic\n// ---------------------------------------------------------------------------\n\nexport interface AnthropicModelManagerConfig {\n\tapiKey?: string;\n\tbaseUrl?: string;\n\tfetch?: FetchImpl;\n}\n\nexport function anthropicModelManagerOptions(\n\tconfig?: AnthropicModelManagerConfig,\n): ModelManagerOptions<\"anthropic-messages\"> {\n\tconst apiKey = config?.apiKey;\n\t// The registry derives `config.baseUrl` from an existing bundled model, and\n\t// bundled Anthropic rows use both `https://api.anthropic.com` and `.../v1`.\n\t// Discovery must always hit `/v1/models`, so the `/v1` suffix is enforced on\n\t// the discovery URL while model rows keep the provider base (#6563).\n\tconst baseUrl = normalizeAnthropicBaseUrl(config?.baseUrl, ANTHROPIC_BASE_URL);\n\tconst discoveryBaseUrl = toAnthropicDiscoveryBaseUrl(baseUrl);\n\treturn {\n\t\tproviderId: \"anthropic\",\n\t\tmodelsDev: {\n\t\t\tfetch: () => fetchWellKnownModels(config?.fetch),\n\t\t\tmap: payload => mapAnthropicModelsDev(payload, baseUrl),\n\t\t},\n\t\t...(apiKey && {\n\t\t\tfetchDynamicModels: async () => {\n\t\t\t\tconst modelsDevModels = await fetchWellKnownModels(config?.fetch)\n\t\t\t\t\t.then(payload => mapAnthropicModelsDev(payload, baseUrl))\n\t\t\t\t\t.catch(() => []);\n\t\t\t\tconst references = buildAnthropicReferenceMap(modelsDevModels);\n\t\t\t\treturn (\n\t\t\t\t\tfetchOpenAICompatibleModels({\n\t\t\t\t\t\tapi: \"anthropic-messages\",\n\t\t\t\t\t\tprovider: \"anthropic\",\n\t\t\t\t\t\tbaseUrl: discoveryBaseUrl,\n\t\t\t\t\t\theaders: buildAnthropicDiscoveryHeaders(apiKey),\n\t\t\t\t\t\tmapModel: (\n\t\t\t\t\t\t\tentry: OpenAICompatibleModelRecord,\n\t\t\t\t\t\t\tdefaults: ModelSpec<\"anthropic-messages\">,\n\t\t\t\t\t\t\t_context: OpenAICompatibleModelMapperContext<\"anthropic-messages\">,\n\t\t\t\t\t\t): ModelSpec<\"anthropic-messages\"> => {\n\t\t\t\t\t\t\tconst discoveredName = typeof entry.display_name === \"string\" ? entry.display_name : defaults.name;\n\t\t\t\t\t\t\tconst reference = references.get(defaults.id);\n\t\t\t\t\t\t\tif (!reference) {\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t...defaults,\n\t\t\t\t\t\t\t\t\tname: discoveredName,\n\t\t\t\t\t\t\t\t\tbaseUrl,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t...reference,\n\t\t\t\t\t\t\t\tid: defaults.id,\n\t\t\t\t\t\t\t\tname: discoveredName,\n\t\t\t\t\t\t\t\tapi: \"anthropic-messages\",\n\t\t\t\t\t\t\t\tprovider: \"anthropic\",\n\t\t\t\t\t\t\t\tbaseUrl,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfetch: config?.fetch,\n\t\t\t\t\t}) ?? null\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Models.dev provider descriptors for generate-models.ts\n// ---------------------------------------------------------------------------\n\n/** Describes how to map models.dev API data for a single provider. */\nexport interface ModelsDevProviderDescriptor {\n\t/** Key in the models.dev API response JSON (e.g., \"anthropic\", \"amazon-bedrock\") */\n\tmodelsDevKey: string;\n\t/** Provider ID in our system */\n\tproviderId: string;\n\t/** Default API type for this provider's models */\n\tapi: Api;\n\t/** Default base URL */\n\tbaseUrl: string;\n\t/** Default context window fallback (default: UNKNNOWN_CONTEXT_WINDOW) */\n\tdefaultContextWindow?: number;\n\t/** Default max tokens fallback (default: UNKNNOWN_MAX_TOKENS) */\n\tdefaultMaxTokens?: number;\n\t/** Optional compat overrides applied to every model from this provider */\n\tcompat?: ModelSpec<Api>[\"compat\"];\n\t/** Optional static headers applied to every model */\n\theaders?: Record<string, string>;\n\t/**\n\t * Optional filter: return false to skip a model.\n\t * Called with (modelId, rawModel). Default: skip if tool_call !== true.\n\t */\n\tfilterModel?: (modelId: string, model: ModelsDevModel) => boolean;\n\t/**\n\t * Optional transform: modify the mapped model before it's added.\n\t * Can return null to skip the model, or an array to emit multiple models.\n\t */\n\ttransformModel?: (\n\t\tmodel: ModelSpec<Api>,\n\t\tmodelId: string,\n\t\traw: ModelsDevModel,\n\t) => ModelSpec<Api> | ModelSpec<Api>[] | null;\n\t/**\n\t * Optional: override the API type per-model.\n\t * Called with (modelId, raw). Return the API type to use.\n\t * If not provided, uses the `api` field.\n\t */\n\tresolveApi?: (modelId: string, raw: ModelsDevModel) => { api: Api; baseUrl: string } | null;\n}\n\n/** Generic mapper that converts models.dev data using provider descriptors. */\nexport function mapModelsDevToModels(\n\tdata: Record<string, unknown>,\n\tdescriptors: readonly ModelsDevProviderDescriptor[],\n): ModelSpec<Api>[] {\n\tconst models: ModelSpec<Api>[] = [];\n\tfor (const desc of descriptors) {\n\t\tconst providerData = (data as Record<string, Record<string, unknown>>)[desc.modelsDevKey];\n\t\tif (!isRecord(providerData) || !isRecord(providerData.models)) continue;\n\n\t\tfor (const [modelId, rawModel] of Object.entries(providerData.models)) {\n\t\t\tif (!isRecord(rawModel)) continue;\n\t\t\tconst m = rawModel as ModelsDevModel;\n\n\t\t\t// Default filter: tool_call must be true\n\t\t\tif (desc.filterModel) {\n\t\t\t\tif (!desc.filterModel(modelId, m)) continue;\n\t\t\t} else {\n\t\t\t\tif (m.tool_call !== true) continue;\n\t\t\t}\n\n\t\t\t// Resolve API and baseUrl (may be per-model for providers like OpenCode)\n\t\t\tconst resolved = desc.resolveApi?.(modelId, m) ?? { api: desc.api, baseUrl: desc.baseUrl };\n\t\t\tif (!resolved) continue;\n\n\t\t\tconst mapped: ModelSpec<Api> = {\n\t\t\t\tid: modelId,\n\t\t\t\tname: toModelName(m.name, modelId),\n\t\t\t\tapi: resolved.api,\n\t\t\t\tprovider: desc.providerId as ModelSpec<Api>[\"provider\"],\n\t\t\t\tbaseUrl: resolved.baseUrl,\n\t\t\t\treasoning: m.reasoning === true,\n\t\t\t\tinput: toInputCapabilities(m.modalities?.input),\n\t\t\t\tcost: {\n\t\t\t\t\tinput: toNumber(m.cost?.input) ?? 0,\n\t\t\t\t\toutput: toNumber(m.cost?.output) ?? 0,\n\t\t\t\t\tcacheRead: toNumber(m.cost?.cache_read) ?? 0,\n\t\t\t\t\tcacheWrite: toNumber(m.cost?.cache_write) ?? 0,\n\t\t\t\t},\n\t\t\t\tcontextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? null),\n\t\t\t\tmaxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? null),\n\t\t\t\t...(m.tool_call === false ? { supportsTools: false } : {}),\n\t\t\t\t...(desc.compat && { compat: desc.compat }),\n\t\t\t\t...(desc.headers && { headers: { ...desc.headers } }),\n\t\t\t};\n\n\t\t\t// Apply per-model transform\n\t\t\tif (desc.transformModel) {\n\t\t\t\tconst result = desc.transformModel(mapped, modelId, m);\n\t\t\t\tif (result === null) continue;\n\t\t\t\tif (Array.isArray(result)) {\n\t\t\t\t\tmodels.push(...result);\n\t\t\t\t} else {\n\t\t\t\t\tmodels.push(result);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tmodels.push(mapped);\n\t\t\t}\n\t\t}\n\t}\n\treturn models;\n}\n\n// Bedrock cross-region prefix helpers\nconst BEDROCK_GLOBAL_PREFIXES = [\n\t\"anthropic.claude-fable-5\",\n\t\"anthropic.claude-mythos-5\",\n\t\"anthropic.claude-haiku-4-5\",\n\t\"anthropic.claude-sonnet-4\",\n\t\"anthropic.claude-opus-4-5\",\n\t\"amazon.nova-2-lite\",\n\t\"cohere.embed-v4\",\n\t\"twelvelabs.pegasus-1-2\",\n];\n\nconst BEDROCK_US_PREFIXES = [\n\t\"amazon.nova-lite\",\n\t\"amazon.nova-micro\",\n\t\"amazon.nova-premier\",\n\t\"amazon.nova-pro\",\n\t\"anthropic.claude-3-7-sonnet\",\n\t\"anthropic.claude-opus-4-1\",\n\t\"anthropic.claude-opus-4-20250514\",\n\t\"deepseek.r1\",\n\t\"meta.llama3-2\",\n\t\"meta.llama3-3\",\n\t\"meta.llama4\",\n];\n\nfunction bedrockCrossRegionId(id: string): string {\n\tif (BEDROCK_GLOBAL_PREFIXES.some(p => id.startsWith(p))) return `global.${id}`;\n\tif (BEDROCK_US_PREFIXES.some(p => id.startsWith(p))) return `us.${id}`;\n\treturn id;\n}\n\ninterface ApiResolutionRule {\n\tmatches: (modelId: string, raw: ModelsDevModel) => boolean;\n\tresolved: { api: Api; baseUrl: string };\n}\n\nfunction resolveApiByRules(\n\tmodelId: string,\n\traw: ModelsDevModel,\n\trules: readonly ApiResolutionRule[],\n\tfallback: { api: Api; baseUrl: string },\n): { api: Api; baseUrl: string } {\n\tfor (const rule of rules) {\n\t\tif (rule.matches(modelId, raw)) return rule.resolved;\n\t}\n\treturn fallback;\n}\n\nfunction createOpenCodeApiResolution(\n\tbasePath: string,\n\tidOverrides: Readonly<Record<string, Api>> = {},\n): {\n\tdefaultResolution: { api: Api; baseUrl: string };\n\trules: ApiResolutionRule[];\n} {\n\tconst completionsBaseUrl = `${basePath}/v1`;\n\t// Per-API base URLs on the OpenCode-style endpoint:\n\t// - openai-completions / openai-responses / google-generative-ai → /v1\n\t// - anthropic-messages → bare basePath (the Anthropic client appends /v1/messages)\n\tconst baseUrlForApi = (api: Api): string => (api === \"anthropic-messages\" ? basePath : completionsBaseUrl);\n\tconst overrideRules: ApiResolutionRule[] = Object.entries(idOverrides).map(([id, api]) => ({\n\t\tmatches: modelId => modelId === id,\n\t\tresolved: { api, baseUrl: baseUrlForApi(api) },\n\t}));\n\treturn {\n\t\tdefaultResolution: { api: \"openai-completions\", baseUrl: completionsBaseUrl },\n\t\trules: [\n\t\t\t// Per-id overrides take precedence over npm-based heuristics so we can\n\t\t\t// correct upstream metadata mismatches (see OPENCODE_GO_API_RESOLUTION).\n\t\t\t...overrideRules,\n\t\t\t{\n\t\t\t\tmatches: (_modelId, raw) => raw.provider?.npm === \"@ai-sdk/openai\",\n\t\t\t\tresolved: { api: \"openai-responses\", baseUrl: completionsBaseUrl },\n\t\t\t},\n\t\t\t{\n\t\t\t\tmatches: (_modelId, raw) => raw.provider?.npm === \"@ai-sdk/anthropic\",\n\t\t\t\tresolved: { api: \"anthropic-messages\", baseUrl: basePath },\n\t\t\t},\n\t\t\t{\n\t\t\t\tmatches: (_modelId, raw) => raw.provider?.npm === \"@ai-sdk/google\",\n\t\t\t\tresolved: { api: \"google-generative-ai\", baseUrl: completionsBaseUrl },\n\t\t\t},\n\t\t],\n\t};\n}\n\n// OpenCode Zen: models.dev declares minimax-m3-free (and forward-compat\n// minimax-m3) with `provider.npm = \"@ai-sdk/anthropic\"`, but the Zen gateway\n// only serves them at https://opencode.ai/zen/v1/chat/completions (verified\n// against the live /v1/models response — minimax-m3-free is listed there, and\n// the gateway has no /v1/messages route for it). Without this override the\n// resolver POSTs anthropic-shaped requests to /v1/messages and the UI surfaces\n// raw <invoke>/<|minimax|>/<tool_call> markup (#1617).\nconst OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution(\"https://opencode.ai/zen\", {\n\t\"minimax-m3\": \"openai-completions\",\n\t\"minimax-m3-free\": \"openai-completions\",\n});\n// OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus\n// (and now also minimax-m3) with `provider.npm = \"@ai-sdk/anthropic\"`, but\n// the OpenCode Go gateway only serves them at\n// `https://opencode.ai/zen/go/v1/chat/completions` (verified against\n// https://opencode.ai/zen/go/v1/models and the upstream endpoint table at\n// https://opencode.ai/docs/go/#endpoints — minimax-m2.5 works the same way\n// and lacks an `npm` field on models.dev so it already falls through to the\n// openai-completions default). Without this override the resolver would POST\n// anthropic-style requests to /v1/messages and the gateway would return its\n// `Page Not Found` HTML (issue #887 for the qwen/m2.7 entries; minimax-m3\n// and minimax-m3-free added under #1617 for the same root cause).\nconst OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution(\"https://opencode.ai/zen/go\", {\n\t\"minimax-m2.7\": \"openai-completions\",\n\t\"minimax-m3\": \"openai-completions\",\n\t\"minimax-m3-free\": \"openai-completions\",\n\t\"qwen3.5-plus\": \"openai-completions\",\n\t\"qwen3.6-plus\": \"openai-completions\",\n});\n\nconst COPILOT_BASE_URL = \"https://api.githubcopilot.com\";\n\nconst COPILOT_DEFAULT_RESOLUTION = {\n\tapi: \"openai-completions\",\n\tbaseUrl: COPILOT_BASE_URL,\n} as const satisfies { api: Api; baseUrl: string };\n\nconst COPILOT_API_RESOLUTION_RULES: readonly ApiResolutionRule[] = [\n\t{\n\t\tmatches: modelId => COPILOT_ANTHROPIC_MODEL_PATTERN.test(modelId),\n\t\tresolved: { api: \"anthropic-messages\", baseUrl: COPILOT_BASE_URL },\n\t},\n\t{\n\t\tmatches: isCopilotResponsesModelId,\n\t\tresolved: { api: \"openai-responses\", baseUrl: COPILOT_BASE_URL },\n\t},\n];\n\nfunction simpleModelsDevDescriptor(\n\tmodelsDevKey: string,\n\tproviderId: string,\n\tapi: Api,\n\tbaseUrl: string,\n\toptions: Omit<ModelsDevProviderDescriptor, \"modelsDevKey\" | \"providerId\" | \"api\" | \"baseUrl\"> = {},\n): ModelsDevProviderDescriptor {\n\treturn {\n\t\tmodelsDevKey,\n\t\tproviderId,\n\t\tapi,\n\t\tbaseUrl,\n\t\t...options,\n\t};\n}\n\nfunction openAiCompletionsDescriptor(\n\tmodelsDevKey: string,\n\tproviderId: string,\n\tbaseUrl: string,\n\toptions: Omit<ModelsDevProviderDescriptor, \"modelsDevKey\" | \"providerId\" | \"api\" | \"baseUrl\"> = {},\n): ModelsDevProviderDescriptor {\n\treturn simpleModelsDevDescriptor(modelsDevKey, providerId, \"openai-completions\", baseUrl, options);\n}\n\nfunction anthropicMessagesDescriptor(\n\tmodelsDevKey: string,\n\tproviderId: string,\n\tbaseUrl: string,\n\toptions: Omit<ModelsDevProviderDescriptor, \"modelsDevKey\" | \"providerId\" | \"api\" | \"baseUrl\"> = {},\n): ModelsDevProviderDescriptor {\n\treturn simpleModelsDevDescriptor(modelsDevKey, providerId, \"anthropic-messages\", baseUrl, options);\n}\n\nconst GOOGLE_VERTEX_BASE_URL = \"https://{location}-aiplatform.googleapis.com\";\nconst GOOGLE_VERTEX_OPENAI_BASE_URL =\n\t\"https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi\";\nconst GOOGLE_VERTEX_ANTHROPIC_BASE_URL =\n\t\"https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/anthropic/models/{model}:streamRawPredict\";\n\nfunction resolveGoogleVertexApi(modelId: string, raw: ModelsDevModel): { api: Api; baseUrl: string } {\n\tif (raw.provider?.npm === \"@ai-sdk/google-vertex/anthropic\") {\n\t\treturn {\n\t\t\tapi: \"anthropic-messages\",\n\t\t\tbaseUrl: GOOGLE_VERTEX_ANTHROPIC_BASE_URL.replace(\"{model}\", modelId),\n\t\t};\n\t}\n\tif (modelId.includes(\"/\") || raw.provider?.npm === \"@ai-sdk/openai-compatible\") {\n\t\treturn { api: \"openai-completions\", baseUrl: GOOGLE_VERTEX_OPENAI_BASE_URL };\n\t}\n\treturn { api: \"google-vertex\", baseUrl: GOOGLE_VERTEX_BASE_URL };\n}\n\nconst MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK: readonly ModelsDevProviderDescriptor[] = [\n\t// --- Amazon Bedrock ---\n\t{\n\t\tmodelsDevKey: \"amazon-bedrock\",\n\t\tproviderId: \"amazon-bedrock\",\n\t\tapi: \"bedrock-converse-stream\",\n\t\tbaseUrl: \"https://bedrock-runtime.us-east-1.amazonaws.com\",\n\t\tfilterModel: (id, m) => {\n\t\t\tif (m.tool_call !== true) return false;\n\t\t\tif (id.startsWith(\"ai21.jamba\")) return false;\n\t\t\tif (id.startsWith(\"amazon.titan-text-express\") || id.startsWith(\"mistral.mistral-7b-instruct-v0\"))\n\t\t\t\treturn false;\n\t\t\treturn true;\n\t\t},\n\t\ttransformModel: (model, modelId, m) => {\n\t\t\tconst crossRegionId = bedrockCrossRegionId(modelId);\n\t\t\tconst bedrockModel: ModelSpec<Api> = {\n\t\t\t\t...model,\n\t\t\t\tid: crossRegionId,\n\t\t\t\tname: toModelName(m.name, crossRegionId),\n\t\t\t};\n\t\t\t// Also emit EU variants for Claude models\n\t\t\tif (modelId.startsWith(\"anthropic.claude-\")) {\n\t\t\t\treturn [\n\t\t\t\t\tbedrockModel,\n\t\t\t\t\t{\n\t\t\t\t\t\t...bedrockModel,\n\t\t\t\t\t\tid: `eu.${modelId}`,\n\t\t\t\t\t\tname: `${toModelName(m.name, modelId)} (EU)`,\n\t\t\t\t\t},\n\t\t\t\t];\n\t\t\t}\n\t\t\treturn bedrockModel;\n\t\t},\n\t},\n];\n\nconst MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor[] = [\n\t// --- Anthropic ---\n\tanthropicMessagesDescriptor(\"anthropic\", \"anthropic\", \"https://api.anthropic.com\", {\n\t\tfilterModel: (id, m) => {\n\t\t\tif (m.tool_call !== true) return false;\n\t\t\tif (\n\t\t\t\tid.startsWith(\"claude-3-5-haiku\") ||\n\t\t\t\tid.startsWith(\"claude-3-7-sonnet\") ||\n\t\t\t\tid === \"claude-3-opus-20240229\" ||\n\t\t\t\tid === \"claude-3-sonnet-20240229\"\n\t\t\t)\n\t\t\t\treturn false;\n\t\t\treturn true;\n\t\t},\n\t}),\n\t// --- Google ---\n\tsimpleModelsDevDescriptor(\n\t\t\"google\",\n\t\t\"google\",\n\t\t\"google-generative-ai\",\n\t\t\"https://generativelanguage.googleapis.com/v1beta\",\n\t),\n\t// --- OpenAI ---\n\tsimpleModelsDevDescriptor(\"openai\", \"openai\", \"openai-responses\", \"https://api.openai.com/v1\"),\n\t// --- Groq ---\n\topenAiCompletionsDescriptor(\"groq\", \"groq\", \"https://api.groq.com/openai/v1\"),\n\t// --- Cerebras ---\n\topenAiCompletionsDescriptor(\"cerebras\", \"cerebras\", \"https://api.cerebras.ai/v1\"),\n\t// --- Together ---\n\topenAiCompletionsDescriptor(\"togetherai\", \"together\", \"https://api.together.xyz/v1\"),\n\t// --- CoreWeave Serverless Inference ---\n\topenAiCompletionsDescriptor(\"wandb\", \"coreweave\", \"https://api.inference.wandb.ai/v1\", {\n\t\ttransformModel: model => {\n\t\t\tif (!model.id.startsWith(\"openai/gpt-oss-\")) {\n\t\t\t\treturn model;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t...model,\n\t\t\t\treasoning: true,\n\t\t\t\tthinking: { mode: \"effort\", efforts: [Effort.Low, Effort.Medium, Effort.High] },\n\t\t\t};\n\t\t},\n\t}),\n\t// --- NVIDIA ---\n\topenAiCompletionsDescriptor(\"nvidia\", \"nvidia\", \"https://integrate.api.nvidia.com/v1\", {\n\t\tdefaultContextWindow: 131072,\n\t}),\n\t// --- xAI ---\n\topenAiCompletionsDescriptor(\"xai\", \"xai\", \"https://api.x.ai/v1\"),\n\t// --- DeepSeek ---\n\topenAiCompletionsDescriptor(\"deepseek\", \"deepseek\", \"https://api.deepseek.com\", {\n\t\t// Only ship the v4 family as built-ins; older deepseek-chat / deepseek-reasoner\n\t\t// ids are kept off the catalog until the issue thread asks for them.\n\t\tfilterModel: (id, m) => m.tool_call === true && id.startsWith(\"deepseek-v4\"),\n\t\tcompat: {\n\t\t\t// DeepSeek V4 effort remapping is derived in model-thinking metadata; this\n\t\t\t// descriptor keeps only transport-shape compat.\n\t\t\tsupportsDeveloperRole: false,\n\t\t\tsupportsReasoningEffort: true,\n\t\t\tmaxTokensField: \"max_tokens\",\n\t\t\t// DeepSeek V4 thinking mode rejects the `tool_choice` control parameter.\n\t\t\t// Tool calls still work without it; the API defaults to auto when tools exist.\n\t\t\tsupportsToolChoice: false,\n\t\t\t// DeepSeek V4's OpenAI format docs enable thinking with both the toggle and\n\t\t\t// reasoning_effort. Keep the toggle explicit for built-in models.\n\t\t\textraBody: { thinking: { type: \"enabled\" } },\n\t\t\t// DeepSeek emits chain-of-thought via `reasoning_content` and requires it\n\t\t\t// to round-trip on assistant tool-call messages so the model can resume\n\t\t\t// from prior thinking (interleaved.field=reasoning_content on models.dev,\n\t\t\t// matches the kimi/openrouter handling already in detectCompat).\n\t\t\treasoningContentField: \"reasoning_content\",\n\t\t\trequiresReasoningContentForToolCalls: true,\n\t\t\trequiresAssistantContentForToolCalls: true,\n\t\t},\n\t}),\n];\n\nconst MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDescriptor[] = [\n\t// --- zAI ---\n\t// Source the models.dev `zai` (pay-as-you-go) key rather than `zai-coding-plan`:\n\t// the coding-plan key reports all-$0 subscription rates, which surface every GLM\n\t// SKU as \"Free\" in `/models`. The PAYG key carries the real per-token rates for\n\t// the identical model ids, so the enumerated token costs line up with the other\n\t// subscription providers for comparison (issue #5598).\n\tanthropicMessagesDescriptor(\"zai\", \"zai\", \"https://api.z.ai/api/anthropic\"),\n\t// --- Umans AI ---\n\t// Source the pay-as-you-go catalog: the coding-plan key publishes subscription\n\t// costs as zero, while `/models/info` omits pricing entirely. The generator\n\t// overlays these rates onto the authoritative endpoint discovery (issue #5733).\n\tanthropicMessagesDescriptor(\"umans-ai\", \"umans\", UMANS_BASE_URL),\n\t// --- Xiaomi ---\n\topenAiCompletionsDescriptor(\"xiaomi\", \"xiaomi\", \"https://api.xiaomimimo.com/v1\", {\n\t\tdefaultContextWindow: 262144,\n\t\tdefaultMaxTokens: 8192,\n\t\tcompat: {\n\t\t\tsupportsStore: false,\n\t\t\tthinkingFormat: \"zai\",\n\t\t\treasoningContentField: \"reasoning_content\",\n\t\t\trequiresReasoningContentForToolCalls: true,\n\t\t\tallowsSyntheticReasoningContentForToolCalls: false,\n\t\t},\n\t}),\n\t// --- MiniMax Coding Plan ---\n\topenAiCompletionsDescriptor(\"minimax-coding-plan\", \"minimax-code\", \"https://api.minimax.io/v1\", {\n\t\tcompat: {\n\t\t\tsupportsStore: false,\n\t\t\tsupportsDeveloperRole: false,\n\t\t\tsupportsReasoningEffort: false,\n\t\t\treasoningContentField: \"reasoning_content\",\n\t\t},\n\t}),\n\topenAiCompletionsDescriptor(\"minimax-cn-coding-plan\", \"minimax-code-cn\", \"https://api.minimaxi.com/v1\", {\n\t\tcompat: {\n\t\t\tsupportsStore: false,\n\t\t\tsupportsDeveloperRole: false,\n\t\t\tsupportsReasoningEffort: false,\n\t\t\treasoningContentField: \"reasoning_content\",\n\t\t},\n\t}),\n\t// --- Alibaba Coding Plan ---\n\topenAiCompletionsDescriptor(\n\t\t\"alibaba-coding-plan\",\n\t\t\"alibaba-coding-plan\",\n\t\t\"https://coding-intl.dashscope.aliyuncs.com/v1\",\n\t\t{\n\t\t\tcompat: {\n\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t},\n\t\t},\n\t),\n\t// --- Zhipu Coding Plan ---\n\topenAiCompletionsDescriptor(\n\t\t\"zhipuai-coding-plan\",\n\t\t\"zhipu-coding-plan\",\n\t\t\"https://open.bigmodel.cn/api/coding/paas/v4\",\n\t\t{\n\t\t\tcompat: {\n\t\t\t\tthinkingFormat: \"zai\",\n\t\t\t\treasoningContentField: \"reasoning_content\",\n\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t},\n\t\t},\n\t),\n];\n\nconst filterActiveToolCallModels = (_id: string, m: ModelsDevModel): boolean => {\n\tif (m.tool_call !== true) return false;\n\tif (m.status === \"deprecated\") return false;\n\treturn true;\n};\n\nconst MODELS_DEV_PROVIDER_DESCRIPTORS_GOOGLE_VERTEX: readonly ModelsDevProviderDescriptor[] = [\n\tsimpleModelsDevDescriptor(\"google-vertex\", \"google-vertex\", \"google-vertex\", GOOGLE_VERTEX_BASE_URL, {\n\t\tfilterModel: filterActiveToolCallModels,\n\t\tresolveApi: resolveGoogleVertexApi,\n\t}),\n];\n\nconst MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDescriptor[] = [\n\t// --- Azure OpenAI ---\n\t// OpenAI-family models hosted on Azure, served via the Responses API. baseUrl\n\t// is empty: the deployment host is per-resource and resolved at runtime from\n\t// AZURE_OPENAI_BASE_URL / AZURE_OPENAI_RESOURCE_NAME (see resolveAzureConfig).\n\tsimpleModelsDevDescriptor(\"azure\", \"azure\", \"azure-openai-responses\", \"\", {\n\t\tfilterModel: (modelId, m) => {\n\t\t\tif (m.tool_call !== true) return false;\n\t\t\t// OpenAI-family only (not Foundry/DeepSeek/Claude/Llama/Mistral/Phi, which\n\t\t\t// Azure serves via non-Responses APIs under a per-model provider override).\n\t\t\treturn /^(gpt-|o1|o3|o4|codex|chatgpt)/.test(modelId);\n\t\t},\n\t}),\n\t// --- Cloudflare AI Gateway ---\n\tanthropicMessagesDescriptor(\n\t\t\"cloudflare-ai-gateway\",\n\t\t\"cloudflare-ai-gateway\",\n\t\t\"https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic\",\n\t),\n\t// --- Mistral ---\n\topenAiCompletionsDescriptor(\"mistral\", \"mistral\", \"https://api.mistral.ai/v1\"),\n\t// --- OpenCode Zen ---\n\topenAiCompletionsDescriptor(\"opencode\", \"opencode-zen\", \"https://opencode.ai/zen/v1\", {\n\t\tfilterModel: filterActiveToolCallModels,\n\t\tresolveApi: (modelId, raw) =>\n\t\t\tresolveApiByRules(\n\t\t\t\tmodelId,\n\t\t\t\traw,\n\t\t\t\tOPENCODE_ZEN_API_RESOLUTION.rules,\n\t\t\t\tOPENCODE_ZEN_API_RESOLUTION.defaultResolution,\n\t\t\t),\n\t}),\n\t// --- OpenCode Go ---\n\topenAiCompletionsDescriptor(\"opencode-go\", \"opencode-go\", \"https://opencode.ai/zen/go/v1\", {\n\t\tfilterModel: filterActiveToolCallModels,\n\t\tresolveApi: (modelId, raw) =>\n\t\t\tresolveApiByRules(\n\t\t\t\tmodelId,\n\t\t\t\traw,\n\t\t\t\tOPENCODE_GO_API_RESOLUTION.rules,\n\t\t\t\tOPENCODE_GO_API_RESOLUTION.defaultResolution,\n\t\t\t),\n\t}),\n\t// --- GitHub Copilot ---\n\topenAiCompletionsDescriptor(\"github-copilot\", \"github-copilot\", COPILOT_BASE_URL, {\n\t\tdefaultContextWindow: 128000,\n\t\tdefaultMaxTokens: 8192,\n\t\theaders: { ...COPILOT_API_HEADERS },\n\t\tfilterModel: filterActiveToolCallModels,\n\t\tresolveApi: (modelId, raw) =>\n\t\t\tresolveApiByRules(modelId, raw, COPILOT_API_RESOLUTION_RULES, COPILOT_DEFAULT_RESOLUTION),\n\t\ttransformModel: model => {\n\t\t\t// compat only applies to openai-completions models\n\t\t\tif (model.api === \"openai-completions\") {\n\t\t\t\treturn {\n\t\t\t\t\t...model,\n\t\t\t\t\tcompat: {\n\t\t\t\t\t\tsupportsStore: false,\n\t\t\t\t\t\tsupportsDeveloperRole: false,\n\t\t\t\t\t\tsupportsReasoningEffort: false,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn model;\n\t\t},\n\t}),\n\t// --- MiniMax (Anthropic) ---\n\tanthropicMessagesDescriptor(\"minimax\", \"minimax\", \"https://api.minimax.io/anthropic\"),\n\tanthropicMessagesDescriptor(\"minimax-cn\", \"minimax-cn\", \"https://api.minimaxi.com/anthropic\"),\n\t// --- Hugging Face ---\n\topenAiCompletionsDescriptor(\"huggingface\", \"huggingface\", \"https://router.huggingface.co/v1\"),\n\t// --- Kilo Gateway ---\n\topenAiCompletionsDescriptor(\"kilo\", \"kilo\", \"https://api.kilo.ai/api/gateway\"),\n\t// --- Moonshot AI ---\n\topenAiCompletionsDescriptor(\"moonshotai\", \"moonshot\", \"https://api.moonshot.ai/v1\"),\n\t// --- NanoGPT ---\n\topenAiCompletionsDescriptor(\"nano-gpt\", \"nanogpt\", \"https://nano-gpt.com/api/v1\"),\n\t// --- Synthetic ---\n\topenAiCompletionsDescriptor(\"synthetic\", \"synthetic\", \"https://api.synthetic.new/openai/v1\"),\n\t// --- Venice AI ---\n\topenAiCompletionsDescriptor(\"venice\", \"venice\", \"https://api.venice.ai/api/v1\", {\n\t\ttransformModel: model => {\n\t\t\tconst maxTokens = clampKimiK27CodeMaxTokens(model.id, model.maxTokens);\n\t\t\treturn maxTokens === model.maxTokens ? model : { ...model, maxTokens };\n\t\t},\n\t}),\n\t// --- Ollama Cloud ---\n\tsimpleModelsDevDescriptor(\"ollama-cloud\", \"ollama-cloud\", \"ollama-chat\", \"https://ollama.com\"),\n\t// --- Xiaomi Token Plan ---\n\topenAiCompletionsDescriptor(\n\t\t\"xiaomi-token-plan-ams\",\n\t\t\"xiaomi-token-plan-ams\",\n\t\t\"https://token-plan-ams.xiaomimimo.com/v1\",\n\t),\n\topenAiCompletionsDescriptor(\n\t\t\"xiaomi-token-plan-cn\",\n\t\t\"xiaomi-token-plan-cn\",\n\t\t\"https://token-plan-cn.xiaomimimo.com/v1\",\n\t),\n\topenAiCompletionsDescriptor(\n\t\t\"xiaomi-token-plan-sgp\",\n\t\t\"xiaomi-token-plan-sgp\",\n\t\t\"https://token-plan-sgp.xiaomimimo.com/v1\",\n\t),\n\t// --- Qwen Portal ---\n\topenAiCompletionsDescriptor(\"qwen-portal\", \"qwen-portal\", \"https://portal.qwen.ai/v1\", {\n\t\tdefaultContextWindow: 128000,\n\t\tdefaultMaxTokens: 8192,\n\t}),\n\n\t// --- ZenMux ---\n\topenAiCompletionsDescriptor(\"zenmux\", \"zenmux\", ZENMUX_OPENAI_BASE_URL, {\n\t\tfilterModel: filterActiveToolCallModels,\n\t\tresolveApi: modelId => {\n\t\t\tif (modelId.startsWith(\"anthropic/\")) {\n\t\t\t\treturn { api: \"anthropic-messages\" as const, baseUrl: ZENMUX_ANTHROPIC_BASE_URL };\n\t\t\t}\n\t\t\treturn { api: \"openai-completions\" as const, baseUrl: ZENMUX_OPENAI_BASE_URL };\n\t\t},\n\t}),\n];\n/** All provider descriptors for models.dev data mapping in generate-models.ts. */\nexport const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] = [\n\t...MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK,\n\t...MODELS_DEV_PROVIDER_DESCRIPTORS_GOOGLE_VERTEX,\n\t...MODELS_DEV_PROVIDER_DESCRIPTORS_CORE,\n\t...MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS,\n\t...MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED,\n];\n"}}, "packages/ai/test/anthropic-unsigned-thinking-replay.test.ts": {"8baa3300bc23c721fc80c0307f0b7a8218be8fcb": {"t": 1785595322.4848375, "n": 10756, "h": "d8ad3ce2fbe1e5dd", "c": "import { describe, expect, it } from \"bun:test\";\nimport { renderDemotedThinking } from \"@oh-my-pi/pi-ai/dialect\";\nimport { convertAnthropicMessages, streamAnthropic } from \"@oh-my-pi/pi-ai/providers/anthropic\";\nimport type {\n\tAssistantMessage,\n\tMessage,\n\tModel,\n\tModelSpec,\n\tToolResultMessage,\n\tUserMessage,\n} from \"@oh-my-pi/pi-ai/types\";\nimport { buildModel } from \"@oh-my-pi/pi-catalog/build\";\n\n/**\n * Regression: Anthropic-compatible reasoning endpoints often emit `thinking`\n * blocks without a first-party Anthropic signature, but still expect those\n * blocks back as native `type: \"thinking\"` on continuation. Demoting unsigned\n * thinking to text strips the reasoning chain and can destabilize follow-up\n * tool-call argument serialization (the upstream cause behind #2005's `todo`\n * renderer crash).\n *\n * Official Anthropic remains conservative: unsigned thinking is demoted to text\n * there because the first-party API enforces signature-based integrity.\n */\nfunction makeModel(overrides: Partial<ModelSpec<\"anthropic-messages\">> = {}): Model<\"anthropic-messages\"> {\n\treturn buildModel({\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"custom-anthropic\",\n\t\tid: \"reasoning-model\",\n\t\tname: \"Reasoning Anthropic-Compatible Model\",\n\t\tbaseUrl: \"https://llm.example.com/anthropic\",\n\t\tinput: [\"text\"],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tmaxTokens: 8_192,\n\t\tcontextWindow: 200_000,\n\t\treasoning: true,\n\t\t...overrides,\n\t} as ModelSpec<\"anthropic-messages\">);\n}\n\nfunction makeUser(text = \"continue\"): UserMessage {\n\treturn { role: \"user\", content: text, timestamp: 0 };\n}\n\nfunction makeAssistantThinking(thinking: string, tail: AssistantMessage[\"content\"][number][] = []): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [{ type: \"thinking\", thinking, thinkingSignature: \"\" }, ...tail],\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"custom-anthropic\",\n\t\tmodel: \"reasoning-model\",\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: \"toolUse\",\n\t\ttimestamp: 0,\n\t};\n}\n\ninterface WireThinkingBlock {\n\ttype: \"thinking\";\n\tthinking: string;\n\tsignature: string;\n}\ninterface WireTextBlock {\n\ttype: \"text\";\n\ttext: string;\n}\ninterface WireToolUseBlock {\n\ttype: \"tool_use\";\n\tid: string;\n\tname: string;\n\tinput: Record<string, unknown>;\n}\ntype WireBlock = WireThinkingBlock | WireTextBlock | WireToolUseBlock | { type: string; [key: string]: unknown };\n\nfunction assistantWireBlocks(messages: Message[], model: Model<\"anthropic-messages\">): WireBlock[] {\n\tconst params = convertAnthropicMessages(messages, model, false);\n\tconst assistant = params.find(p => p.role === \"assistant\");\n\treturn (assistant?.content as WireBlock[] | undefined) ?? [];\n}\n\ndescribe(\"Anthropic-compatible unsigned thinking replay (#2005)\", () => {\n\tit(\"preserves unsigned thinking for non-official reasoning endpoints\", () => {\n\t\tconst blocks = assistantWireBlocks(\n\t\t\t[\n\t\t\t\tmakeUser(\"solve x\"),\n\t\t\t\tmakeAssistantThinking(\"plan: read the file, then edit\", [{ type: \"text\", text: \"Sure.\" }]),\n\t\t\t],\n\t\t\tmakeModel(),\n\t\t);\n\t\texpect(blocks[0]).toEqual({\n\t\t\ttype: \"thinking\",\n\t\t\tthinking: \"plan: read the file, then edit\",\n\t\t\tsignature: \"\",\n\t\t});\n\t\texpect(blocks[1]).toEqual({ type: \"text\", text: \"Sure.\" });\n\t});\n\n\tit(\"sends context_management for API-key Anthropic-compatible thinking requests\", async () => {\n\t\tconst { promise, resolve } = Promise.withResolvers<unknown>();\n\t\tstreamAnthropic(\n\t\t\tmakeModel(),\n\t\t\t{ systemPrompt: [], messages: [makeUser(\"continue\")] },\n\t\t\t{\n\t\t\t\tapiKey: \"sk-ant-api-test\",\n\t\t\t\tsignal: AbortSignal.abort(),\n\t\t\t\tthinkingEnabled: true,\n\t\t\t\tonPayload: payload => resolve(payload),\n\t\t\t},\n\t\t);\n\n\t\tconst payload = (await promise) as {\n\t\t\tthinking?: { type?: string };\n\t\t\tcontext_management?: { edits?: Array<{ type?: string; keep?: string }> };\n\t\t};\n\t\texpect(payload.thinking?.type).toBe(\"enabled\");\n\t\texpect(payload.context_management).toEqual({\n\t\t\tedits: [{ type: \"clear_thinking_20251015\", keep: \"all\" }],\n\t\t});\n\t});\n\n\tit(\"sanitizes lone surrogates in tool arguments regardless of origin API\", () => {\n\t\tconst loneSurrogate = \"broken \\ud83d end\";\n\t\tconst makeToolCallAssistant = (api: AssistantMessage[\"api\"]): AssistantMessage => ({\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: \"call_1\",\n\t\t\t\t\tname: \"write\",\n\t\t\t\t\targuments: { text: loneSurrogate, nested: { parts: [loneSurrogate] } },\n\t\t\t\t},\n\t\t\t],\n\t\t\tapi,\n\t\t\tprovider: api === \"anthropic-messages\" ? \"custom-anthropic\" : \"openai\",\n\t\t\tmodel: \"reasoning-model\",\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"toolUse\",\n\t\t\ttimestamp: 0,\n\t\t});\n\n\t\t// Cross-API replay: Anthropic's strict UTF-8 validation rejects lone\n\t\t// surrogates, so string leaves are deep-sanitized.\n\t\tconst crossBlocks = assistantWireBlocks([makeUser(), makeToolCallAssistant(\"openai-responses\")], makeModel());\n\t\tconst crossToolUse = crossBlocks.find(block => block.type === \"tool_use\") as WireToolUseBlock;\n\t\texpect(crossToolUse.input.text).toBe(\"broken \\ufffd end\");\n\t\texpect((crossToolUse.input.nested as { parts: string[] }).parts[0]).toBe(\"broken \\ufffd end\");\n\n\t\t// Same-API replay sanitizes too: the model itself can emit lone-surrogate\n\t\t// escapes in its own tool-argument JSON (streamed out fine, 400 on replay).\n\t\tconst sameBlocks = assistantWireBlocks([makeUser(), makeToolCallAssistant(\"anthropic-messages\")], makeModel());\n\t\tconst sameToolUse = sameBlocks.find(block => block.type === \"tool_use\") as WireToolUseBlock;\n\t\texpect(sameToolUse.input.text).toBe(\"broken \\ufffd end\");\n\t\texpect((sameToolUse.input.nested as { parts: string[] }).parts[0]).toBe(\"broken \\ufffd end\");\n\t});\n\n\tit(\"sanitizes payload replacements before SDK JSON serialization\", async () => {\n\t\tconst splitSurrogate = `render \\`\"icon.ghost\": \"\\\\ud83d${String.fromCharCode(0xdc7b)}\"\\``;\n\t\tconst { promise: bodyPromise, resolve } = Promise.withResolvers<string>();\n\t\tconst controller = new AbortController();\n\t\tconst fakeFetch = async (_input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n\t\t\tconst raw = init?.body;\n\t\t\tresolve(typeof raw === \"string\" ? raw : new TextDecoder().decode(raw as Uint8Array));\n\t\t\tcontroller.abort();\n\t\t\treturn new Response('event: message_stop\\ndata: {\"type\":\"message_stop\"}\\n\\n', {\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: { \"content-type\": \"text/event-stream\" },\n\t\t\t});\n\t\t};\n\n\t\tstreamAnthropic(\n\t\t\tmakeModel(),\n\t\t\t{ systemPrompt: [], messages: [makeUser(\"continue\")] },\n\t\t\t{\n\t\t\t\tapiKey: \"sk-ant-test\",\n\t\t\t\tsignal: controller.signal,\n\t\t\t\tfetch: fakeFetch,\n\t\t\t\tonPayload: payload => {\n\t\t\t\t\tconst request = payload as { messages?: Array<{ role: string; content: unknown }> };\n\t\t\t\t\tif (!Array.isArray(request.messages)) throw new Error(\"Anthropic payload missing messages\");\n\t\t\t\t\trequest.messages.push({\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"tool_use\",\n\t\t\t\t\t\t\t\tid: \"toolu_surrogate\",\n\t\t\t\t\t\t\t\tname: \"write\",\n\t\t\t\t\t\t\t\tinput: { text: splitSurrogate },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\treturn request;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tconst body = await bodyPromise;\n\t\texpect(body).not.toContain(\"\\\\udc7b\");\n\t\texpect(body).toContain(\"\\ufffd\");\n\t});\n\n\tit(\"covers the Xiaomi MiMo Anthropic-compatible reporter configuration without provider allowlists\", () => {\n\t\tconst model = makeModel({\n\t\t\tprovider: \"user-custom\",\n\t\t\tid: \"mimo-v2.5-pro\",\n\t\t\tname: \"MiMo V2.5 Pro (Singapore)\",\n\t\t\tbaseUrl: \"https://token-plan-sgp.xiaomimimo.com/anthropic\",\n\t\t\tmaxTokens: 131_072,\n\t\t\tcontextWindow: 1_048_576,\n\t\t});\n\t\tconst blocks = assistantWireBlocks([makeUser(), makeAssistantThinking(\"hidden reasoning\")], model);\n\t\texpect(blocks[0]).toEqual({ type: \"thinking\", thinking: \"hidden reasoning\", signature: \"\" });\n\t});\n\n\tit(\"preserves legacy known non-signing endpoints even if model.reasoning is false\", () => {\n\t\tconst model = makeModel({ provider: \"custom\", baseUrl: \"https://api.deepseek.com/v1\", reasoning: false });\n\t\tconst blocks = assistantWireBlocks([makeUser(), makeAssistantThinking(\"deepseek reasoning\")], model);\n\t\texpect(blocks[0]?.type).toBe(\"thinking\");\n\t});\n\n\tit(\"still degrades unsigned thinking to text for official Anthropic\", () => {\n\t\tconst model = makeModel({ provider: \"anthropic\", baseUrl: \"https://api.anthropic.com\" });\n\t\tconst blocks = assistantWireBlocks([makeUser(), makeAssistantThinking(\"internal scratch\")], model);\n\t\texpect(blocks[0]?.type).toBe(\"text\");\n\t\texpect((blocks[0] as WireTextBlock).text).toBe(renderDemotedThinking(model.id, \"internal scratch\"));\n\t});\n\n\tit(\"treats a missing baseUrl as official Anthropic (resolveAnthropicBaseUrl default)\", () => {\n\t\t// `isOfficialAnthropicApiUrl(undefined) === true` because the actual HTTP\n\t\t// dispatch falls back to https://api.anthropic.com. Same-id custom\n\t\t// overrides that only tweak model metadata (no baseUrl override) must\n\t\t// not regress to native-thinking replay against the first-party API.\n\t\tconst model = makeModel({ provider: \"anthropic\", baseUrl: \"\" });\n\t\tconst blocks = assistantWireBlocks([makeUser(), makeAssistantThinking(\"internal scratch\")], model);\n\t\texpect(blocks[0]?.type).toBe(\"text\");\n\t\texpect((blocks[0] as WireTextBlock).text).toBe(renderDemotedThinking(model.id, \"internal scratch\"));\n\t});\n\n\tit(\"still degrades unsigned thinking to text for non-reasoning unknown endpoints\", () => {\n\t\tconst model = makeModel({ reasoning: false, baseUrl: \"https://plain.example.com/anthropic\" });\n\t\tconst blocks = assistantWireBlocks([makeUser(), makeAssistantThinking(\"scratch\")], model);\n\t\texpect(blocks[0]?.type).toBe(\"text\");\n\t\texpect((blocks[0] as WireTextBlock).text).toBe(renderDemotedThinking(model.id, \"scratch\"));\n\t});\n\n\tit(\"keeps thinking → tool_use pairing intact across continuation conversion\", () => {\n\t\tconst toolResult: ToolResultMessage = {\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId: \"toolu_reasoning_1\",\n\t\t\ttoolName: \"read\",\n\t\t\tcontent: [{ type: \"text\", text: \"file body\" }],\n\t\t\tisError: false,\n\t\t\ttimestamp: 0,\n\t\t};\n\t\tconst model = makeModel();\n\t\tconst messages: Message[] = [\n\t\t\tmakeUser(\"read README\"),\n\t\t\tmakeAssistantThinking(\"I need to call the read tool\", [\n\t\t\t\t{ type: \"toolCall\", id: \"toolu_reasoning_1\", name: \"read\", arguments: { path: \"README.md\" } },\n\t\t\t]),\n\t\t\ttoolResult,\n\t\t];\n\t\tconst params = convertAnthropicMessages(messages, model, false);\n\t\texpect(params.map(p => p.role)).toEqual([\"user\", \"assistant\", \"user\"]);\n\t\tconst assistantBlocks = params[1].content as WireBlock[];\n\t\texpect(assistantBlocks[0]?.type).toBe(\"thinking\");\n\t\texpect(assistantBlocks[1]?.type).toBe(\"tool_use\");\n\t\texpect((assistantBlocks[1] as WireToolUseBlock).id).toBe(\"toolu_reasoning_1\");\n\t});\n});\n"}}, "packages/catalog/CHANGELOG.md": {"8baa3300bc23c721fc80c0307f0b7a8218be8fcb": {"t": 1785595999.2042835, "n": 78686, "h": "6db1195c980b97e1", "c": "# Changelog\n\n## [Unreleased]\n\n## [17.2.3] - 2026-08-01\n\n### Added\n\n- Added support for the ai& provider (`aiand`), an OpenAI-compatible inference API with dynamic model discovery (context windows, capabilities, reasoning efforts, and USD pricing from `/v1/models`) and API-key authentication via the `AIAND_API_KEY` environment variable.\n\n## [17.2.2] - 2026-07-31\n\n### Added\n\n- Added support for the GMI Cloud provider (`gmi-cloud`), an OpenAI-compatible inference gateway with dynamic model discovery and API-key authentication via the `GMI_API_KEY` environment variable.\n- Added optional authoritative context occupancy to usage records for providers with separate checkpoint telemetry and billable token buckets.\n\n### Fixed\n\n- Fixed classification of dynamically discovered Cursor Kimi K3 effort variants as non-reasoning models when `thinkingDetails` is omitted.\n- Fixed Google AI Studio OpenAI-compatible requests failing with HTTP 400 by omitting the unsupported `store` field.\n- Fixed Synthetic models losing capabilities (such as reasoning/thinking selectors, vision input, output limits, and pricing) by correcting how the discovery mapper parses Synthetic's advertised features, effort vocabularies, and pricing structures.\n- Fixed Cursor model discovery to correctly expose the 1M-token context window for supported models (including Claude, GPT, Kimi K3, and GLM 5.2+ families) instead of defaulting to 200k.\n- Fixed GitHub Copilot routing for `grok-4.5` to use the correct Responses endpoint instead of the unsupported Chat Completions endpoint.\n\n## [17.2.1] - 2026-07-30\n\n### Fixed\n\n- Fixed Ollama model-manager caches being reused after the configured base URL changed by scoping cache namespaces to the normalized native discovery endpoint, including reverse-proxy path prefixes ([#7087](https://github.com/can1357/oh-my-pi/issues/7087)).\n\n## [17.2.0] - 2026-07-30\n\n### Added\n\n- Regenerated the Cursor agent protobufs (`discovery/cursor-gen/agent_pb.ts`) against the modern `agent.proto`, adding the message and enum families current Cursor CLI builds emit: Pi tool exec frames, hook queries and responses, subagents, allowlist prechecks, MCP state, smart-mode classification, canvas diagnostics, conversation search, agent-store conflicts and git diff. Purely additive — no existing exported symbol changed shape.\n\n### Fixed\n\n- Fixed an issue where LM Studio first turns failed with a 400 Invalid tool_choice error when a named tool was forced, by using the supported tool_choice: \"required\" string selector.\n\n## [17.1.8] - 2026-07-28\n\n### Added\n\n- Added `resolveVertexEndpointHost(location)` utility to resolve the correct Vertex AI API endpoint hostnames for global, multi-region, and regional locations.\n\n### Fixed\n\n- Fixed an issue where `calculateCost` under-reported Anthropic cache-write costs by honoring the `usage.cttl` breakdown to correctly price 1-hour retention writes at 2x the base input rate.\n\n## [17.1.7] - 2026-07-27\n\n### Added\n\n- Added support for moonshotai/Kimi-K3 and kimi-k3-fast models\n- Added umans-kimi-k3 prerelease model configuration\n\n### Changed\n\n- Updated pricing and token limits for selected models\n\n## [17.1.6] - 2026-07-27\n\n### Added\n\n- Added SiliconFlow providers (`siliconflow`, `siliconflow-cn`) with dynamic-only OpenAI-compatible model discovery: no bundled catalog — the model list is fetched live from each region's `/v1/models` endpoint, with non-chat entries (embedding, reranker, image, audio, video) filtered out. Discovery hydrates pricing, context/output limits, and reasoning metadata from the provider's models.dev catalog at runtime (with bundled upstream references as a reasoning-only fallback for ids models.dev has not indexed), so reasoning models keep thinking enabled and sessions compact against real context windows. `SILICONFLOW_API_KEY` / `SILICONFLOW_CN_API_KEY` environment variables are wired into `getEnvApiKey`.\n\n## [17.1.5] - 2026-07-27\n\n### Fixed\n\n- Fixed Kimi Code (`kimi-code`) reporting `maxTokens: 32000` for every model — its `/coding/v1/models` discovery mapper and the bundled catalog applied a blanket constant, truncating `k3`/`k3-256k` output at ~4x below their real 131072 ceiling and `kimi-for-coding`/`kimi-for-coding-highspeed` below their 32768 ceiling. Output caps are now derived per family, and the model cache is invalidated so upgrades drop the stale `maxTokens: 32000` rows (including the discovery-only `k3-256k`) instead of serving them until the next network refresh ([#6711](https://github.com/can1357/oh-my-pi/issues/6711)).\n- Fixed Anthropic model discovery 404ing when the registry derived the provider base URL from a bundled model without the `/v1` suffix (`https://api.anthropic.com/models` instead of `/v1/models`), which let a stale text-only cache row shadow fresh models.dev vision metadata — surfacing as snapcompact refusing to run on `claude-opus-5`. Discovery now always targets `/v1/models` while model rows keep the provider base URL ([#6563](https://github.com/can1357/oh-my-pi/issues/6563)).\n\n## [17.1.4] - 2026-07-26\n\n### Added\n\n- Added Claude Opus 5 model entries for Amazon Bedrock: `anthropic.claude-opus-5` plus its `us.`, `eu.`, `au.`, and `global.` regional/geo IDs.\n\n### Fixed\n\n- Fixed `alibaba-token-plan` locking out China (Beijing) 百炼 Token Plan subscribers: the provider hardcoded the international Singapore endpoint, so Beijing-issued `sk-sp-` keys got `401 invalid_api_key`. The wire credential now carries an optional region base URL, and model discovery targets the credential's region ([#6682](https://github.com/can1357/oh-my-pi/issues/6682)).\n- Fixed forced `tool_choice` 400s (`tool_choice 'specified' is incompatible with thinking enabled`) on Kimi Code's Anthropic-compatible endpoint for the `kimi-for-coding`, `kimi-for-coding-highspeed`, and `k3` aliases: the Anthropic-surface compat matcher only recognised Moonshot's native `kimi-k2.7-code*` ids, so thinking-locked kimi-code models kept `supportsForcedToolChoice: true` and the forced selector was sent to a host that always thinks. These models now resolve `requiresThinkingEnabled`, keeping thinking on and downgrading forced choices to `auto`.\n- Retried empty successful provider discovery responses after the short non-authoritative interval instead of caching them for the full catalog TTL ([#6620](https://github.com/can1357/oh-my-pi/issues/6620)).\n- Fixed GitHub Copilot Claude models with no bundled catalog reference (e.g. a freshly served `claude-opus-5`) discovering with `reasoning: false`/`thinking: null` and no effort dial, and disappearing along with their synthesized `-1m` sibling on offline reads: reference-less Copilot models on the anthropic-messages proxy now derive the adaptive reasoning ladder from the model id, and the cache restores their compile-time `COPILOT_API_HEADERS` by value instead of dropping them as unrestorable ([#6664](https://github.com/can1357/oh-my-pi/issues/6664)).\n\n## [17.1.3] - 2026-07-24\n\n### Fixed\n\n- Disabled the first-event watchdog for local OpenAI-compatible backends while retaining the 300-second inter-event watchdog, so long llama.cpp prompt prefill is not canceled and retried ([#6524](https://github.com/can1357/oh-my-pi/issues/6524)).\n\n## [17.1.1] - 2026-07-24\n\n### Added\n\n- Added catalog metadata for models that support native computer-use requests.\n- Added resolved Bedrock Converse prompt-cache compatibility limits, including explicit 5-minute checkpoint support for bundled Nova Lite, Micro, Pro, Premier, and Nova 2 Lite models plus their documented in-region, regional, and global IDs, and model-specific 1-hour Claude retention.\n- Added resolved Bedrock Converse prompt-cache compatibility limits, including explicit 5-minute checkpoint support for bundled Nova Lite, Micro, Pro, and Premier models plus Nova Premier's documented in-region model ID, and model-specific 1-hour Claude retention.\n- Added the native Meta Model API provider and Muse Spark 1.1 with Responses API reasoning replay, image input, and the full supported reasoning-effort ladder ([#4941](https://github.com/can1357/oh-my-pi/issues/4941)).\n- Added an opt-in Vercel AI Gateway automatic prompt-cache compatibility option alongside provider routing preferences.\n- Added Vercel AI Gateway Responses cache-anchor and cache-lifetime compatibility controls.\n- Added resolved OpenAI GPT-5.6 prompt-cache breakpoint capability metadata, keeping older models and compatible endpoints opt-in only.\n- Added the native `alibaba-token-plan` provider with QwenCloud Token Plan Individual discovery and a curated chat-model fallback catalog ([#6151](https://github.com/can1357/oh-my-pi/issues/6151)).\n\n## [17.1.0] - 2026-07-24\n\n### Added\n\n- Added Bedrock Converse prompt-cache compatibility limits, including 5-minute checkpoint support for Nova models (Lite, Micro, Pro, Premier, Nova 2 Lite) and 1-hour Claude retention.\n- Added native Meta Model API provider and Muse Spark 1.1 support, featuring Responses API reasoning replay, image input, and reasoning-effort controls.\n- Added Vercel AI Gateway integration features, including opt-in automatic prompt-cache compatibility, provider routing preferences, and Responses cache-anchor and cache-lifetime controls.\n- Added prompt-cache breakpoint capability metadata for OpenAI GPT-5.6, with opt-in support for older models and compatible endpoints.\n- Added native alibaba-token-plan provider with QwenCloud Token Plan Individual discovery and a curated chat-model fallback catalog.\n\n## [17.0.9] - 2026-07-23\n\n### Changed\n\n- Renamed `codex-auto-review` model to `GPT-5.3 Codex Spark` with updated pricing and capabilities\n- Removed image input support from GPT-5.3 Codex Spark (text-only)\n- Reduced GPT-5.3 Codex Spark context window from 272K to 128K tokens\n- Changed GPT-5.3 Codex Spark thinking efforts from `[\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"]` to `[\"low\", \"medium\", \"high\", \"xhigh\"]`\n- Updated pricing for multiple AI models across providers (costs adjusted in models.json)\n- Reduced max output tokens for an unspecified model from 16384 to 8192\n- Added image input support to Venice AI text model\n\n## [17.0.8] - 2026-07-22\n\n### Added\n\n- Added support for several new models across multiple providers, including MiniMax M3, Gemini 3.5 Flash Lite, Gemini 3.6 Flash (with thinking support), Hy3, Doubao-Seed-Character, LongCat 2.0, Laguna S 2.1 (free and paid tiers), Qwen 3.6 35B A3B, SWE-1.6 Slow (devin agent catalog), and XiaomiMiMo/MiMo-V2.5.\n\n### Changed\n\n- Updated Grok 4.5 API type to \"openai-responses\" and updated \"o3-mini\" to support thinking capabilities with the \"kimi\" thinking format.\n- Renamed OpenRouter-specific models and routers to include \"OpenRouter\" in their names (e.g., \"OpenRouter Auto Router (Beta)\", \"OpenRouter Body Builder (beta)\", and \"OpenRouter Pareto Code Router\").\n- Updated context window sizes, costs, and token limits for numerous models.\n\n### Fixed\n\n- Fixed an issue where GPT-5.6 Codex SKUs lost usable context window capacity due to dynamic discovery values overwriting bundled limits.\n- Fixed OpenAI Codex discovery dropping account-listed ChatGPT-only models (such as GPT-5.3 Codex Spark) when they are unavailable through the public API.\n- Fixed Codex catalog discovery hiding models when multiple OAuth accounts are configured by independently fetching and merging catalogs from all accounts.\n- Fixed cached models reusing a bundled request model (such as GitHub Copilot long-context variants) being incorrectly flagged as unrestorable and dropped after a restart.\n- Fixed LM Studio discovery reporting a model's theoretical maximum context length instead of the actual loaded context window size of the running instance.\n\n### Removed\n\n- Removed several deprecated model families from the devin catalog, including Claude Fable 5, Claude Opus 4.6/4.7, Claude Sonnet 4.6/5, DeepSeek V4 Pro, Gemini 3.1 Pro, Gemini 3.5 Flash, GLM-5.2, SWE-1.6, and Nemotron 3 Ultra.\n- Removed GPT-5 through GPT-5.3 Codex variants and GPT-5.4 nano from the openai-codex catalog.\n\n## [17.0.6] - 2026-07-20\n\n### Added\n\n- Added static fallback seed for Devin's `swe-1-7` model so it is bundled even when catalog generation runs without a Devin session token.\n\n### Fixed\n\n- Collapsed Devin's six GLM-5.2 variants into two logical entries (`glm-5-2` for 200K free, `glm-5-2-1m` for 1M paid). The 200K entry routes every thinking effort to the free `glm-5-2` wire UID — never to the quota-gated `glm-5-2-max` or `glm-5-2-none` — so GLM-5.2 works even when the weekly usage quota is exhausted.\n\n## [17.0.5] - 2026-07-18\n\n### Added\n\n- Added an Anthropic compatibility flag to allow non-official OAuth endpoints to opt into configured Claude Code fingerprint header overrides.\n\n### Fixed\n\n- Fixed a security issue where sensitive provider-defined request headers (such as API keys or credentials) were serialized in plaintext within the model cache (models.db). The cache now omits these headers, securely invalidates older cached rows, and restores or refetches them dynamically.\n- Fixed OpenAI Codex discovery to respect caller-supplied fetch configurations (such as proxies or custom CAs) and correctly replace stale bundled models with the authenticated account catalog.\n- Fixed stream timeouts and retry loops during long prefills on local loopback or RFC1918 backends (such as litellm proxies fronting local servers) by applying the local stream-timeout floor to these backends.\n- Fixed Kimi K3 models served through generic OpenAI-compatible routes exposing unsupported reasoning efforts instead of the mandatory low/high/max scale.\n\n## [17.0.4] - 2026-07-18\n\n### Changed\n\n- Kimi-family models now use MFJS tool schema on all hosts, including proxies like OpenRouter that forward schemas to Moonshot\n\n## [17.0.3] - 2026-07-17\n\n### Fixed\n\n- Logged LiteLLM rich-metadata endpoint failures once with their endpoint and status before falling back to incomplete `/v1/models` data ([#5801](https://github.com/can1357/oh-my-pi/issues/5801)).\n- Fixed authenticated Kimi Code discovery to preserve live effort levels, default effort, mandatory-thinking state, and per-model protocol metadata ([#5893](https://github.com/can1357/oh-my-pi/issues/5893)).\n- Fixed LiteLLM provider ignoring per-model pricing: `mapLiteLLMRichEntry` now reads `input_cost_per_token` / `output_cost_per_token` (plus cache costs) from LiteLLM rich metadata and maps them to `cost.input` / `cost.output`, falling back to the bundled reference only when LiteLLM omits cost, so proxied models no longer display as free ([#5818](https://github.com/can1357/oh-my-pi/issues/5818)).\n\n## [17.0.2] - 2026-07-17\n\n### Changed\n\n- Increased the maximum output tokens (maxTokens) from 32,768 to 65,536 for Kimi K2.7-Code models on Fireworks.\n\n### Fixed\n\n- Fixed a regression where the context window for openai-codex GPT-5.6 models (Luna, Sol, Terra) incorrectly fell back to 272,000 instead of preserving its 372,000 capacity.\n- Fixed Umans PAYG models incorrectly displaying as \"Free\" in /models by correctly sourcing their published per-token rates.\n- Fixed native moonshot/kimi-k3 capabilities and pricing, ensuring it correctly reflects its official pricing, 1M context window, image input support, reasoning capabilities, and 128k output token limit.\n\n## [17.0.1] - 2026-07-16\n\n### Added\n\n- Added GPT-5.6 Luna, Sol, and Terra entries for Amazon Bedrock, Azure, and Cloudflare\n- Added KAT-Coder Air/Pro V2.5 entries across Kilo, OpenRouter, NanoGPT, and Vercel\n- Added Inkling model entries for Baseten and Vercel AI Gateway\n- Added Umans DeepSeek V4 Pro DSpark as an experimental model listing\n- Added Claude Opus 4.7 Fast and 4.8 Fast on Vercel AI Gateway\n- Added Workers AI GLM-5.2, Muse Spark 1.1, Stealth GPT-5.6 Sol, and nano-gpt-help entries\n\n### Changed\n\n- Added image input and reasoning support to several existing Codeium and Kilo GPT-5.6 models\n- Enabled image input and reasoning for Gemini Flash Latest and Grok 4.5\n- Renamed many model labels for consistency, including Claude, Grok, DeepSeek, GLM, and Gemi­ni names\n- Updated pricing for many existing models, including input, output, and cache cost values\n- Updated context window and max token limits for many catalog models across providers\n\n### Fixed\n\n- Fixed Z.AI (GLM) coding-plan token costs all showing as \"Free\" in `/models`: the `zai` provider descriptor sourced the models.dev `zai-coding-plan` key (all-$0 subscription rates) instead of the `zai` pay-as-you-go key, which carries the real per-token rates for the identical GLM ids ([#5598](https://github.com/can1357/oh-my-pi/issues/5598)).\n- Fixed custom Anthropic endpoints receiving the first-party-only `eager_input_streaming` tool field by default ([#5572](https://github.com/can1357/oh-my-pi/issues/5572)).\n- Added resolved OpenAI sampling-parameter compatibility metadata for o-series and GPT-5+ models.\n- Fixed GitHub Copilot `mai-code-1-flash-picker` (and other `mai-*` models) to route through the `/responses` endpoint instead of `/chat/completions`, which rejected them with `400 unsupported_api_for_model` ([#5612](https://github.com/can1357/oh-my-pi/issues/5612)).\n- Extended the reasoning `streamIdleTimeoutMs` floor (300s) to native Kimi K2.7 Code (`kimi-k2.7-code` / `kimi-k2.7-code-highspeed`), which previously fell through to the 120s default and aborted on long reasoning turns ([#4836](https://github.com/can1357/oh-my-pi/issues/4836)).\n- Fixed GLM-5.x coding-plan streams via the OpenCode Go/Zen gateways (`opencode.ai/zen/…`) timing out with `OpenAI completions stream stalled while waiting for the next event` during slow plan-writing/reasoning phases. The 600s idle-timeout floor for GLM coding-plan SKUs was gated to the native Z.AI/Zhipu hosts only, so OpenCode-fronted GLM fell back to the 120s default watchdog. ([#4758](https://github.com/can1357/oh-my-pi/issues/4758))\n\n## [16.5.2] - 2026-07-14\n\n### Fixed\n\n- Fixed OpenCode Zen and Go discovery to replace stale bundled models with each provider's live model catalog.\n\n## [16.5.1] - 2026-07-14\n\n### Fixed\n\n- Fixed reasoning effort mapping for Z.ai GLM-5.2 on the Anthropic messages endpoint to correctly use the two-tier scale (high, max) and emit output_config.effort.\n- Fixed an issue where stale cached model limits would override updated static catalog limits after a catalog fingerprint mismatch.\n- Fixed Cursor discovery to correctly preserve GetUsableModels max-mode metadata for premium models and invalidate stale cache entries.\n\n## [16.4.3] - 2026-07-11\n\n### Fixed\n\n- Fixed parsing of SAP AI Core Claude model IDs in version-first format (e.g., anthropic--claude-4.8-opus), restoring adaptive thinking metadata and capability gates.\n- Fixed GitHub Copilot Business and Enterprise model discovery to correctly preserve vision capabilities instead of downgrading models to text-only.\n\n## [16.4.2] - 2026-07-10\n\n### Fixed\n\n- Fixed OpenAI Codex model discovery to include the Codex version header alongside the client_version query parameter.\n\n## [16.4.1] - 2026-07-10\n\n### Added\n\n- Added GPT-5.6 Luna, Sol, and Terra models\n- Added perplexity-academic-researcher model\n\n### Changed\n\n- Updated context windows for multiple GPT-5.6 models\n- Increased max tokens for several models\n- Updated cache write costs for GPT-5.6 variants\n- Reduced pricing for select models\n\n### Removed\n\n- Removed the generated GPT-5.6 pro-reasoning aliases (`gpt-5.6-{luna,sol,terra}-pro`) from the `openai-codex` subscription provider — pro reasoning is not offered on subscriptions; the `openai` API-key aliases remain\n\n## [16.4.0] - 2026-07-10\n\n### Breaking Changes\n\n- Redesigned reasoning effort ladders to be wire-exact, removing the shifted five-tier effort mapping. Models now expose exactly the effort tiers their upstream APIs accept, mapped 1:1. Removed SHIFTED_FIVE_TIER_EFFORT_MAP, ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER, and per-host xhigh-to-max alias maps. Selecting an unsupported tier now automatically clamps down via clampThinkingLevelForModel. Devin effort routing is now mapped 1:1 onto per-tier siblings.\n\n### Added\n\n- Added support for new models: Grok 4.5 family, Dolphin Mistral 24b Venice Edition, GLM5.2-Fast, and Zenmux variants for GPT-5.6 (Luna, Sol, and Terra).\n- Added Novita as a model provider, including public catalog discovery, pricing, limits, modality, reasoning, and tool metadata.\n- Added useResponsesLite to Model and ModelSpec to support the Responses Lite transport, enabled by default for the GPT-5.6 family.\n- Added Effort.Max (\"max\") as a first-class user-facing thinking level above xhigh.\n\n### Changed\n\n- Enabled reasoning effort controls for Grok 4.5 and updated support flags for additional Grok variants\n- Standardized reasoning effort levels to use a wire-exact max tier across all model providers, including Devin routing and Ollama configurations.\n- Updated costs and context windows for various models in the catalog.\n\n## [16.3.15] - 2026-07-09\n\n### Added\n\n- Added support for Grok 4.5 model\n- Added `gpt-5.6` base models and `gpt-5.6-{luna,sol,terra}-pro` variants\n- Added `meta/muse-spark-1.1` model support\n- Added support for thinking modes on `poolside/laguna` models\n- Added generated GPT-5.6 Pro aliases (`gpt-5.6-{luna,sol,terra}-pro`) on the `openai` and `openai-codex` providers: each alias sends the base model id on the wire (`requestModelId`) with the new `reasoningMode: \"pro\"` marker, and re-derives from the current base rows on every catalog regeneration.\n\n### Changed\n\n- Updated cache read costs for Grok models\n- Reduced max token limit for Grok 4.3 model\n- Enabled prompt cache affinity for Grok models via the x-grok-conv-id header in OpenAI compatible endpoints\n- Enabled prompt cache affinity for Grok models via the x-grok-conv-id header\n- Marked direct xAI Grok Chat Completions models for `x-grok-conv-id` prompt-cache affinity.\n\n## [16.3.14] - 2026-07-09\n\n### Added\n\n- Added support for GPT-5.6 (Luna, Sol, Terra) model variants\n- Enabled expanded five-tier reasoning effort scale (minimal to xhigh) for GPT-5.6 models\n- Added GPT-5.6 (Terra/Luna/Sol) support for the new `max` reasoning tier: on wire-effort APIs (OpenAI Responses, Codex, Azure, openai-compat/OpenRouter models that advertise reasoning) user efforts shift up one notch — `xhigh` sends `max`, `high` sends `xhigh` — mirroring the Claude Fable/Opus 4.7+ five-tier mapping, and the exposed ladder becomes `minimal..xhigh` with `minimal` reaching the native `low` tier. Devin's per-tier GPT-5.6 sibling rows now collapse into `gpt-5-6-{luna,sol,terra}` logical models with the same shifted routing (`xhigh` → `-max`), plus `-fast` families that keep the direct `low..xhigh` `-priority` scale since Devin serves no `-max-priority` tier.\n\n## [16.3.13] - 2026-07-09\n\n### Added\n\n- Added support for Grok 4.5 across multiple providers\n- Added support for GPT-5.6 series models (Luna, Sol, Terra)\n- Added Aion 3.0 and 3.0 Mini models\n- Added Kuaishou KAT-Coder v2.5 models\n- Added Nex-N2-Mini and SWE-1.7 series models\n- Added Hy3 models and free variants\n\n### Changed\n\n- Updated cost and token configurations for various models across providers\n- Renamed several models for consistency (e.g., MiniMax M3, Gemma 4 31B, Qwen variants)\n\n## [16.3.12] - 2026-07-08\n\n### Fixed\n\n- Fixed LiteLLM discovery stopping at `/model_group/info` when that endpoint omitted `supports_vision`; it now continues to `/model/info` and preserves `model_info.supports_vision=true` for vision-capable proxy models. ([#4747](https://github.com/can1357/oh-my-pi/issues/4747))\n- Fixed LiteLLM discovery to fall back to bundled catalog metadata when `models.dev` lacks a model reference, preserving reasoning and thinking support for models such as `glm-5.2`. ([#4695](https://github.com/can1357/oh-my-pi/issues/4695))\n- Detected Azure AI Inference / Foundry Anthropic routes as strict-tool-incompatible so resolved Anthropic compat disables strict tools before request construction ([#4679](https://github.com/can1357/oh-my-pi/issues/4679)).\n\n## [16.3.11] - 2026-07-06\n\n### Added\n\n- Added Claude Haiku 4.5 (JP) model support\n- Added tencent/hy3 model support via ZenMux\n\n### Changed\n\n- Updated naming format for various synthetic models to include provider prefix\n- Adjusted context window limit for MiniMax-M3 model\n- Updated pricing for select models\n\n## [16.3.10] - 2026-07-06\n\n### Fixed\n\n- Fixed LiteLLM rich discovery to ignore unusable sentinel placeholders and continue to `/v2/model/info` for real models. ([#4655](https://github.com/can1357/oh-my-pi/issues/4655))\n\n## [16.3.9] - 2026-07-06\n\n### Fixed\n\n- Fixed compatibility with OpenCode Go DeepSeek V4 models by sending max_tokens instead of max_completion_tokens to match the provider's API requirements.\n\n## [16.3.7] - 2026-07-05\n\n### Fixed\n\n- Fixed usage cost calculation to correctly account for provider orchestration token sidecars without misclassifying them as standard input, output, or cache tokens.\n\n## [16.3.4] - 2026-07-03\n\n### Added\n\n- Added Baseten as a supported model provider\n- Added support for new models from Baseten, including DeepSeek V4 Pro and Kimi series\n- Added new Devin agent models: Claude 5 Fable variants\n- Added new Github Copilot models: Kimi K2.7 Code and MAI-Code-1-Flash\n- Added Poolside Laguna XS 2.1 models via Kilo and OpenRouter providers\n- Added support for Claude Fable 5 (Free) via Zenmux provider\n\n### Changed\n\n- Updated priority ordering to include Baseten\n- Updated pricing and limits for various existing models in the catalog\n\n## [16.3.3] - 2026-07-02\n\n### Fixed\n\n- Extended Anthropic-compatible signing-endpoint recognition to Cloudflare AI Gateway, Google Vertex, AWS Bedrock, and Azure AI Inference / Foundry to ensure consistent reasoning-replay and signature-stripping behavior, and exposed ResolvedAnthropicCompat.signingEndpoint in the public API.\n- Fixed Zhipu Coding Plan runtime discovery to prioritize account-scoped model lists over bundled fallback models, preventing routing errors for valid non-z.ai keys.\n\n## [16.3.2] - 2026-07-02\n\n### Fixed\n\n- Fixed ZenMux model discovery to run without a `ZENMUX_API_KEY`, so newly published ZenMux models (for example `anthropic/claude-fable-5-free`) auto-update into the runtime `models.db` cache instead of waiting on a regenerated `models.json`.\n- Fixed ZenMux runtime discovery to query the `/api/v1/models` endpoint even when the resolved provider base URL points at the Anthropic-compatible route, so discovery no longer requests a non-existent `/api/anthropic/models` path.\n\n## [16.3.1] - 2026-07-02\n\n### Removed\n\n- Removed reasoning suppression prompt logic for GPT-5 models\n\n## [16.3.0] - 2026-07-02\n\n### Breaking Changes\n\n- Renamed the `requiresJuiceZeroHack` compatibility flag to `requiresReasoningSuppressionPrompt` (affecting `OpenAICompat` and `ResolvedOpenAIResponsesCompat`) and removed the unused `\"juice-zero-developer-message\"` member from `OpenAIReasoningDisableMode`.\n\n### Fixed\n\n- Fixed stream markup healing pattern misfires by disabling the healer on the official OpenAI endpoint.\n- Updated the Xiaomi provider's default model to the supported `mimo-v2.5` model.\n- Fixed model discovery probes (including Ollama and metadata fetches) failing behind private-CA gateways by ensuring they honor the `NODE_EXTRA_CA_CERTS` environment variable.\n- Fixed CoreWeave Serverless Inference project-header detection to ensure blank OpenAI-Project overrides do not block the `COREWEAVE_PROJECT` fallback.\n- Fixed LiteLLM MiniMax M3 discovery to remove reseller-only display suffixes and invalidated the model cache to clear stale suffixes immediately.\n- Fixed ZenMux's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`), matching the GitHub Copilot fix (#2851). ZenMux's `zenmux.ai/api/anthropic` route forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: \"\"` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused `400 messages.1.content.0: Invalid signature in thinking` on Claude Sonnet 5 and other reasoning models. ([#4192](https://github.com/can1357/oh-my-pi/issues/4192))\n\n## [16.2.13] - 2026-07-01\n\n### Added\n\n- Added support for human-readable reasoning summaries on compatible OpenAI Codex models (v5.4+)\n\n### Fixed\n\n- Fixed discovered OpenAI Codex models to advertise V2 streaming remote compaction, avoiding the legacy compact endpoint timeout path for Codex sessions. ([#4146](https://github.com/can1357/oh-my-pi/issues/4146))\n\n## [16.2.12] - 2026-07-01\n\n### Breaking Changes\n\n- Removed runtime canonical-equivalence APIs from the identity module, including resolveCanonicalVariant, buildCanonicalModelOrder, CanonicalVariantPreferences, and getBundledCanonicalReferenceData. These utilities have been transitioned to a build-time generator script and are no longer exposed in the runtime bundle.\n\n## [16.2.11] - 2026-07-01\n\n### Fixed\n\n- Fixed a potential memory leak caused by dangling timeout timers during model discovery in OpenAI-compatible, vLLM, LiteLLM, and LM Studio catalogs.\n- Widened stream watchdogs for local OpenAI-compatible backends (including llama.cpp, LM Studio, vLLM, and Ollama) to prevent premature timeouts during cold model loads.\n\n## [16.2.10] - 2026-06-30\n\n### Added\n\n- Added Claude Sonnet 3.7, Claude Opus 3, and Claude Sonnet 3 model entries to the Anthropic catalog\n- Added Anthropic Claude Sonnet 5 model entry to the Kilo provider catalog\n- Added first-party catalog discovery support for the Anthropic provider\n- Added Gemini 3.1 Flash Lite Image model entry to the Kilo provider catalog\n- Added Anthropic Claude Sonnet 5 model variants with low, medium, high, xhigh, and max thinking efforts to the Devin provider catalog\n- Added Claude Sonnet 5 model entry to the Anthropic curated catalog.\n\n### Changed\n\n- Updated the base API URL for the Claude Sonnet 5 model in the Anthropic catalog\n- Updated pricing metrics for DeepSeek R1 and DeepSeek V3 model entries to reflect new rates\n\n## [16.2.9] - 2026-06-30\n\n### Added\n\n- Added full capability support for Claude Sonnet 5, aligning it with Claude Opus 4.8 and Fable 5. This includes adaptive thinking display, mid-conversation system messages, sampling parameter and thinking omission API restrictions, and 5-tier adaptive reasoning effort mapping (including xhigh and max levels) across direct APIs, OpenRouter, and Bedrock Converse.\n\n### Changed\n\n- Updated input and output costs for models in the catalog.\n\n## [16.2.7] - 2026-06-30\n\n### Fixed\n\n- Fixed compatibility with Kimi K2.7 Code on native endpoints to ensure thinking mode is preserved and tool choice is not forced.\n- Fixed Cerebras gemma-4-31b dynamic discovery to correctly identify the model as image-capable, enabling proper serialization of attached images.\n\n## [16.2.6] - 2026-06-29\n\n### Fixed\n\n- Fixed namespaced GLM-5.x model IDs on Z.AI/Zhipu OpenAI-compatible endpoints to inherit the widened stream watchdog, avoiding spurious stalled-stream errors during long thinking phases. ([#3819](https://github.com/can1357/oh-my-pi/issues/3819))\n\n## [16.2.3] - 2026-06-28\n\n### Added\n\n- Added support and configuration parameters for V2 streaming compaction in RemoteCompactionConfig, catalog types, and model/provider metadata.\n\n### Changed\n\n- Enabled automatic content markup healing for all OpenAI-compatible streaming models\n- Updated pricing and context window limits for several catalog models.\n- Disabled reasoning capability for multiple providers in the catalog.\n\n## [16.2.2] - 2026-06-27\n\n### Removed\n\n- Removed 'pi' from the list of supported dialects.\n\n## [16.2.0] - 2026-06-27\n\n### Added\n\n- Added GitLab Duo Agent catalog discovery, including namespace selection, live model mapping, and a bundled fallback model for fresh installs.\n- Added OpenAICompat.supportsNamedToolChoice to support forced tool use on string-only OpenAI-compatible chat servers without emitting the named function-object tool_choice shape.\n- Added model metadata support for provider-native remote compaction and compaction-only model selection.\n\n### Changed\n\n- Disabled the thinking-effort selector for GitLab Duo Agent models since the underlying platform parameters are server-fixed.\n\n### Fixed\n\n- Improved GitLab Duo Agent and Duo Workflow namespace and project discovery to robustly handle paginated groups, SSH remotes with custom ports, Git worktrees, self-managed GitLab instances with relative paths, and configuration via GITLAB_DUO_PROJECT_PATH or GITLAB_DUO_PROJECT_ID.\n- Fixed built-in LiteLLM discovery to prefer rich proxy metadata from management endpoints and avoid caching stale capability data.\n- Fixed GitLab Duo Workflow model specifications to resolve correct static context windows, enabling accurate context usage tracking and auto-compaction.\n\n## [16.1.23] - 2026-06-26\n\n### Added\n\n- Added `OpenAICompat.qwenPreserveThinking` — auto-enabled when the resolved `thinkingFormat` is `\"qwen\"` or `\"qwen-chat-template\"` AND `replayReasoningContent` is on (i.e. the four built-in local OpenAI-compatible providers, or a custom provider pointed at a loopback / RFC1918 / `*.local` baseUrl). Pairs with the chat-completions encoder change so the request body carries `preserve_thinking: true` (twin top-level + `chat_template_kwargs` emission), keeping Qwen3.6+ from stripping `<think>...</think>` off older assistant turns and breaking the local slot's KV cache between user messages. Non-Qwen chat templates ignore the parameter, so the flag stays a no-op outside the Qwen path; users on a cloud Qwen host (Alibaba Dashscope / Qwen Portal) can opt in with `compat.qwenPreserveThinking: true`. ([#3541](https://github.com/can1357/oh-my-pi/issues/3541))\n- Added CoreWeave Serverless Inference as an OpenAI-compatible provider with models.dev-backed bundled catalog metadata.\n\n## [16.1.22] - 2026-06-26\n\n### Added\n\n- Added `OpenAICompat.replayReasoningContent` — auto-enabled for the built-in local OpenAI-compatible providers (`llama.cpp`, `lm-studio`, `vllm`, `ollama` on `openai-completions`) and for any provider pointed at a loopback / RFC1918 / `*.local` baseUrl. NOT gated on `spec.reasoning`: the runtime discovery paths for `llama.cpp` / `lm-studio` / `openai-models-list` hardcode `reasoning: false` because the upstream `/models` endpoints don't advertise the capability, while the stream parser still records incoming `reasoning_content` deltas as thinking blocks — gating on the spec flag would leave every discovered local Qwen / DeepSeek model re-triggering #3528. The encoder only writes `reasoning_content` when a thinking block actually exists on the turn, so the flag is a no-op on pure-text histories. Built-in proxy providers (currently `litellm`) are excluded from both checks because they forward to an unrelated upstream that gains no KV-cache benefit and may 400 on the extra field; users running a custom proxy in front of a llama.cpp-style backend can opt in via the sparse `compat.replayReasoningContent: true` override. Signals to the `openai-completions` encoder that preserved `thinking` blocks must be re-emitted as `reasoning_content` on every assistant turn so chat templates that reconstruct `<think>…</think>` from the field (Qwen3, DeepSeek-R1, GLM-5.x) keep the prior turn's tokens byte-stable and llama.cpp's prefix KV cache survives. ([#3528](https://github.com/can1357/oh-my-pi/issues/3528))\n\n## [16.1.20] - 2026-06-25\n\n### Fixed\n\n- Fixed direct Anthropic Claude Sonnet/Haiku 4.5 advisor/agent turns crashing every call with HTTP 400 `This model does not support the effort parameter.` The catalog classified the whole Claude 4.5 family on `anthropic-messages` (and `bedrock-converse-stream`) as `anthropic-budget-effort`, which made the Anthropic provider serialize `output_config.effort` alongside `thinking.budget_tokens`. Anthropic only honors `output_config.effort` on Opus 4.5 and adaptive (4.6+) Messages-API models, so Sonnet 4.5 / Haiku 4.5 rejected the field. `inferThinkingControlMode` now gates `anthropic-budget-effort` to `parsedModel.kind === \"opus\" && semverGte(version, \"4.5\")` on both Anthropic-routed APIs, so Sonnet 4.5 / Haiku 4.5 on direct Anthropic + Cloudflare-AI-Gateway + Vertex + GitLab-Duo + Copilot + Bedrock fall through to plain `mode: \"budget\"` (thinking budget still scales with the selected effort tier). Opus 4.5 keeps `anthropic-budget-effort`. `anthropic-budget-effort` also stays in use for Anthropic-compatible third-party backends that natively support the field (Umans GLM 5.2). ([#3497](https://github.com/can1357/oh-my-pi/issues/3497))\n\n## [16.1.17] - 2026-06-24\n\n### Fixed\n\n- Fixed the Umans GLM-5.2 thinking-level picker collapsing to a single `high` tier after dynamic discovery: the `max` upstream level now resolves to the internal `xhigh` effort, the picker shows both `high` and `xhigh`, and the metadata maps `xhigh` back to Umans's native `max` wire tier. ([#3192](https://github.com/can1357/oh-my-pi/issues/3192))\n- Fixed GitHub Copilot business and enterprise endpoints accepting image inputs that they reject with `400 vision is not supported`. The Copilot `/models` response advertises `capabilities.supports.vision = true` for Claude/GPT chat models on every host, but only the canonical personal endpoint (`https://api.githubcopilot.com`) actually serves them; `githubCopilotModelManagerOptions` now forces `input: [\"text\"]` whenever discovery resolves to a non-personal base URL, and `mergeDynamicModel` honours the dynamic value (instead of OR-upgrading) when the merged endpoint differs from the bundled reference. ([#3387](https://github.com/can1357/oh-my-pi/issues/3387))\n- Fixed OpenRouter Anthropic compat to strip Responses reasoning history during replay so signed thinking blocks are not sent back to routed Anthropic providers. ([#3399](https://github.com/can1357/oh-my-pi/issues/3399))\n\n## [16.1.14] - 2026-06-22\n\n### Added\n\n- Added Sakana AI provider support with Fugu model integration\n- Added Sakana AI/Fugu provider catalog entries with Fugu model discovery and Responses API metadata\n- Added support for \"xhigh\" reasoning tier across model configurations\n- Added configuration for new models GCP-5.4 Mini, GPT-5.5, and variants\n- Added `devin` variant collapse table to streamline model tiering\n\n### Changed\n\n- Updated reasoning label pattern to include \"minimal\" and \"max\" efforts\n- Simplified model identification logic for Devin-powered reasoning models\n- Refactored variant routing to consolidate and standardize tier definitions\n\n## [16.1.13] - 2026-06-22\n\n### Added\n\n- Added support for Devin as a model provider\n- Added capability to fetch dynamic models from the Devin model manager\n\n## [16.1.11] - 2026-06-21\n\n### Fixed\n\n- Fixed Umans `umans-glm-5.1` / `umans-glm-5.2` advertising native image input. The `models/info` endpoint reports `supports_vision: \"via-handoff\"` for the GLM models, meaning vision routes through a separate handoff pre-analysis step instead of accepting raw image blocks; `umansSupportsVision` treated any non-empty string as native vision support, so image prompts went directly to GLM and were rejected with `400 This model does not support image inputs`. The helper now requires `supports_vision === true`, the bundled GLM 5.1/5.2 rows are corrected to text-only, and stale mismatched Umans cache rows for those ids are dropped so the vision-handoff path runs even before a successful refresh. ([#3184](https://github.com/can1357/oh-my-pi/issues/3184))\n\n## [16.1.9] - 2026-06-21\n\n### Fixed\n\n- Fixed the `moonshot` provider with no path to the Kimi China API: model discovery now honors a `MOONSHOT_BASE_URL` override (redirecting to `api.moonshot.cn`), and `KIMI_API_KEY` resolves as a fallback for `MOONSHOT_API_KEY`. ([#2883](https://github.com/can1357/oh-my-pi/issues/2883))\n- Fixed LiteLLM model discovery preserving colliding models.dev transport metadata (for example `ollama-cloud` `deepseek-v4-flash`) instead of keeping the LiteLLM `openai-completions` provider transport. ([#3162](https://github.com/can1357/oh-my-pi/issues/3162))\n\n### Removed\n\n- Removed bundled Wafer Pass (`wafer-pass`) catalog entries and generation support; Wafer Serverless remains available as `wafer-serverless`.\n\n## [16.1.8] - 2026-06-20\n\n### Fixed\n\n- Fixed Fireworks-hosted Qwen turns (e.g. `fireworks/qwen3.7-plus`) failing with `400 Extra inputs are not permitted, field: 'enable_thinking'`. Fireworks serves Qwen3 with controllable thinking via OpenAI-style `reasoning_effort` and rejects the top-level `enable_thinking` boolean that Alibaba DashScope speaks; `buildOpenAICompat` was selecting `thinkingFormat: \"qwen\"` from the `qwen` id pattern regardless of host. Fireworks-hosted Qwen models now resolve to `thinkingFormat: \"openai\"`.\n- Fixed MiMo models on OpenAI-compatible gateways to expose only accepted `low`, `medium`, and `high` reasoning tiers and map unsupported raw `minimal`/`xhigh` requests to safe wire values. ([#2864](https://github.com/can1357/oh-my-pi/issues/2864))\n\n## [16.1.7] - 2026-06-20\n\n### Fixed\n\n- Fixed MiniMax-M3 catalog context for the MiniMax Coding/Token Plan providers `minimax-code` and `minimax-code-cn` to report the documented 1M long-context tier instead of the upstream 512K pricing boundary; the previous patch only covered `minimax`/`minimax-cn`, so the Coding Plan picker still showed 512K in the status bar ([#3097](https://github.com/can1357/oh-my-pi/issues/3097)).\n\n## [16.1.4] - 2026-06-19\n\n### Fixed\n\n- Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families so every effort and off resolve to the live wire id. Added `claude-sonnet-4-6` and `claude-opus-4-6-thinking` entries to `ANTIGRAVITY_MODEL_WIRE_PROFILES` capped at the backend's 64000-output-token limit (over-cap requests 400'd with `Request contains an invalid argument`); `modelEnum` is now optional on `AntigravityModelWireProfile` since the Claude wire ids are accepted without a captured `labels.model_enum`. ([#3067](https://github.com/can1357/oh-my-pi/issues/3067))\n\n## [16.1.3] - 2026-06-19\n\n### Fixed\n\n- Marked Ollama Cloud catalog models to omit on-the-wire output-token caps, preventing context-window-sized `num_predict` values from causing HTTP 400s for models whose true output cap is not discoverable. ([#2984](https://github.com/can1357/oh-my-pi/issues/2984))\n- Fixed `readModelCache`/`writeModelCache` using a process-global shared database even when a custom `dbPath` was provided. Custom-path cache operations now open and close a per-call database via `withModelCacheDb`, preventing leaked SQLite handles on Windows\n\n## [16.1.2] - 2026-06-19\n\n### Added\n\n- Added support for Gemini 2.5 Flash-Lite, 3.1 Flash-Lite, and 3.5 Flash models\n- Added support for Moonshot V1 model family\n\n### Changed\n\n- Updated context window and token limits for various Claude, Gemini, and GPT-OSS models\n- Refined thinking mode behaviors and routing for supported LLM families\n\n### Fixed\n\n- Fixed GLM-5.2 `reasoning_effort` so the top thinking tier reaches each host's genuine maximum instead of 400ing, mapping the internal `xhigh` tier per host dialect (verified against live endpoints): Z.ai/Zhipu collapse onto the model's `none`/`high`/`max` scale (`xhigh → max`); Fireworks, resellers, and Ollama Cloud keep their distinct lower tiers and remap only the top `xhigh → max` (merged over host quirks such as Fireworks' `minimal → none`); and OpenRouter — whose API rejects `max` and treats `xhigh` as its own max tier — now exposes the `xhigh` tier and forwards it verbatim. Dialect detection keys off resolved `compat.thinkingFormat`, so custom OpenRouter/Z.ai-format providers are covered too.\n- Maintained thinking effort routing when discovery only returns the base model ID\n- Improved credential retrieval logic for Antigravity and Codex providers via auth discovery\n\n## [16.0.9] - 2026-06-18\n\n### Fixed\n\n- Fixed GitHub Copilot's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`). It forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: \"\"` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused a `400 Invalid signature` that corrupted the session and re-tripped on every full history re-send (e.g. after toggling MCP servers). Copilot now degrades such blocks to text like the official API. ([#2851](https://github.com/can1357/oh-my-pi/issues/2851))\n- Added a `supportsImageDetailOriginal` compat flag that resolves to `false` for GitHub Copilot, whose Responses endpoint rejects the `detail: \"original\"` image hint with a 400, and `true` for every other host. ([#2822](https://github.com/can1357/oh-my-pi/issues/2822))\n\n## [16.0.8] - 2026-06-18\n\n### Changed\n\n- Refactored model family ID predicates and capability checkers to use a shared, uniform process-lifetime `memo` utility to eliminate caching boilerplate.\n\n### Fixed\n\n- Fixed LM Studio dynamic discovery to use native `/api/v0/models` metadata so VLM models advertise image input. ([#2945](https://github.com/can1357/oh-my-pi/issues/2945))\n\n## [16.0.7] - 2026-06-18\n\n### Fixed\n\n- Fixed MiniMax Anthropic-compatible M2/M3 thinking metadata to expose the adaptive transport and keep M2 mandatory reasoning floored ([#2928](https://github.com/can1357/oh-my-pi/issues/2928)).\n\n## [16.0.6] - 2026-06-18\n\n### Added\n\n- Added a dedicated `openrouter` API type and `ResolvedOpenRouterCompat` configuration to support unified chat-completions and Responses-API compatibility for OpenRouter models\n\n### Changed\n\n- Migrated bundled OpenRouter models in the catalog from `openai-completions` to the new `openrouter` API type\n- Consolidated the resolved OpenAI compat shape: extracted a shared `ResolvedOpenAISharedCompat` core that both `ResolvedOpenAICompat` and `ResolvedOpenAIResponsesCompat` extend (each builder still computes its own per-surface value, preserving chat↔Responses divergence), added internal resolved wire-quirk fields (`wireModelIdMode`, `stripDeepseekSpecialTokens`, `reasoningDeltasMayBeCumulative`, `emptyLengthFinishIsContextError`, `usesOpenAIToolCallIdLimit`, `dropThinkingWhenReasoningEffort`, `supportsObfuscationOptOut`), and replaced `buildOpenRouterCompat`'s cast-and-copy with an exhaustive `pickResponsesOnly` composition that fails to compile if a new Responses-only field is added without handling. The public `OpenAICompat` config vocabulary is unchanged.\n- Expanded `OpenAICompat`/`ResolvedOpenAISharedCompat` with shared reasoning/history/stream/request flags (`reasoningDisableMode`, `omitReasoningEffort`, `includeEncryptedReasoning`, `filterReasoningHistory`, `requiresReasoningContentForAllAssistantTurns`, `streamMarkupHealingPattern`, `promptCacheSessionHeader`, etc.) so model/provider/gateway constraints are declared once in catalog compat and then consumed uniformly by Chat Completions and Responses endpoints.\n\n### Fixed\n\n- Changed the default compatibility builder for `openai-completions` to set `requiresAssistantAfterToolResult` to `isMistral`, enabling the synthetic assistant bridge for built-in Mistral and Devstral models.\n- Fixed local Ollama (`provider: \"ollama\"`) reasoning turns still failing with HTTP 400 `invalid reasoning value: \"minimal\"` when the model was selected from a stale `~/.omp/models.db` cache row or a hand-written config: the `minimal → low` / `xhigh → max` remap was only stamped during fresh discovery, so cached and custom specs reached the wire unmapped. The remap now lives in the OpenAI chat-completions and Responses compat builders, so every `buildModel` (including cache loads, custom specs, and the `whenThinking` variant) backfills it — no `omp models refresh` required. Custom OpenAI-compatible providers registered under a non-`ollama` provider id still need their own `compat.reasoningEffortMap`.\n- Advertised Ollama Cloud GLM-5.2 reasoning efforts as high/xhigh-only and mapped `xhigh` to native max effort ([#2911](https://github.com/can1357/oh-my-pi/pull/2911) by [@serverinspector](https://github.com/serverinspector))\n- Fixed OpenRouter pseudo-API model construction so bundled OpenRouter models resolve shared OpenAI compatibility metadata instead of an undefined compat record.\n- Fixed custom/direct `xai-oauth` Responses model specs (e.g. `grok-build`) emitting `reasoning.effort` and hitting xAI's HTTP 400: `buildOpenAIResponsesCompat` now defaults `supportsReasoningEffort` to `false` for `xai-oauth` Grok models that are off the effort-capable allowlist (`grok-3-mini`/`grok-4.20-multi-agent`/`grok-4.3`), matching the curated discovery path; explicit `compat.supportsReasoningEffort` still overrides. The allowlist moved to a shared `isGrokReasoningEffortCapable` identity helper consumed by both the compat builder and provider-model curation so the two cannot drift.\n\n## [16.0.5] - 2026-06-17\n\n### Added\n\n- Added `enableGeminiThinkingLoopGuard` to OpenAI compatibility options to allow explicit opt-in or opt-out of the Gemini thinking-loop guard for OpenAI-compatible model aliases\n- Added `LITELLM_BASE_URL` as the LiteLLM provider discovery base URL fallback, with discovery caches scoped by the resolved proxy URL and explicit provider `baseUrl` config kept at higher precedence. ([#2726](https://github.com/can1357/oh-my-pi/issues/2726))\n- Added `ThinkingConfig.effortBudgets` (per-effort thinking-budget contract baked into collapsed variants) and `ANTIGRAVITY_MODEL_WIRE_PROFILES` (`maxOutputTokens` + `model_enum` per Antigravity wire id) to mirror the captured Antigravity Cloud Code Assist client request shape.\n\n### Changed\n\n- Defaulted `enableGeminiThinkingLoopGuard` from Gemini family detection for both OpenAI completions and responses compatibility specs so Gemini models now enable the thinking-loop guard automatically\n- Updated the default Gemini CLI user-agent version fallback to 0.46.0.\n- Changed the Antigravity (`google-antigravity`, daily-cloudcode-pa) gemini-3.x collapse families to the `budget` thinking transport with the client's per-tier `thinkingBudget` (3.5 Flash low/medium/high = 1000/4000/10000, 3.1 Pro low/high = 1001/10001) and corrected 3.5 Flash effort→wire routing (medium → `gemini-3.5-flash-low`, high → `gemini-3-flash-agent`). Split the shared CCA collapse table so `google-gemini-cli` (cloudcode-pa) keeps the `google-level` `thinkingLevel` transport for official Gemini CLI parity. Stale collapsed snapshots (bundled catalog, recycled `gemini-3-flash` alias) self-heal from the hand table at collapse time, and the model cache schema is bumped to v7 to invalidate pre-budget Antigravity rows.\n- Changed the Antigravity user-agent to the `antigravity/hub/<version>` format (default `2.1.4`) to match the captured client.\n\n### Fixed\n\n- Fixed `off` effort routing for `claude-opus-4-5` and `claude-opus-4-6` to use their base model IDs when thinking is disabled\n- Fixed `gemini-2.5-flash` effort routing so all non-off effort levels resolve to `gemini-2.5-flash-thinking`\n- Fixed shared variant alias provider resolution so `resolveBareVariantAlias` reports all matching providers when model aliases are present in both CCA collapse tables\n- Routed google-antigravity default baseUrl to the stable primary daily endpoint in the catalog generator and all fallback snapshots, resolving connection drops on heavy queries.\n- Fixed MiniMax M3 dialect selection so MiniMax-family OpenAI-compatible models use the MiniMax tool-call dialect instead of generic XML. ([#2759](https://github.com/can1357/oh-my-pi/issues/2759))\n- Fixed GitHub Copilot dynamic discovery to honor plan-specific API endpoints stored in structured OAuth credentials. ([#2876](https://github.com/can1357/oh-my-pi/issues/2876))\n\n## [16.0.4] - 2026-06-17\n\n### Fixed\n\n- Fixed GLM-5.2 catalog thinking metadata for Zhipu/BigModel so the top effort is exposed as `xhigh` and maps to provider-native `max`. ([#2833](https://github.com/can1357/oh-my-pi/issues/2833))\n\n## [16.0.2] - 2026-06-16\n\n### Fixed\n\n- Fixed Kimi output caps for Umans AI Coding Plan and Venice so discovery metadata cannot use context-sized token ceilings as request caps.\n- Marked Umans Anthropic-compatible models as client-tool escaped so cached and bundled metadata do not expose `web_search` as a provider server tool.\n\n## [16.0.1] - 2026-06-15\n\n### Added\n\n- Added the Umans AI Coding Plan provider catalog with Anthropic-compatible model metadata and dynamic discovery ([#2636](https://github.com/can1357/oh-my-pi/pull/2636) by [@oldschoola](https://github.com/oldschoola)).\n\n## [16.0.0] - 2026-06-15\n\n### Breaking Changes\n\n- Renamed the catalog-owned tool syntax API from `ToolCallSyntax`/`FALLBACK_TOOL_SYNTAX`/`preferredToolSyntax` to `Dialect`/`FALLBACK_DIALECT`/`preferredDialect`.\n\n## [15.13.3] - 2026-06-15\n\n### Added\n\n- Added Azure OpenAI as a catalog provider (`azure`, default model `gpt-5.5`, env var `AZURE_OPENAI_API_KEY`), bundling the OpenAI-family models Azure serves over the Responses API (GPT-4/4.1/4o, GPT-5 family, o-series, Codex). Like Amazon Bedrock it is catalog-only — models ship in the bundle and become selectable once the env key is set, with the deployment base URL resolved at runtime from `AZURE_OPENAI_BASE_URL`/`AZURE_OPENAI_RESOURCE_NAME`.\n- Added models.dev-backed bundled catalogs for providers that previously shipped no offline models: Hugging Face, Kilo, Moonshot, NanoGPT, Synthetic, Venice, Ollama Cloud, and the Xiaomi Token Plan regions (ams/cn/sgp). They still discover live when credentialed; the bundle is now a non-empty baseline.\n\n### Changed\n\n- Updated stale provider default models to their latest bundled versions: OpenAI-family providers (`azure`, `github-copilot`, `aimlapi`) → GPT-5.5; Gemini providers (`google`, `google-gemini-cli`, `google-vertex`) → `gemini-3.1-pro-preview`; GLM providers (`zai`, `zhipu-coding-plan`) → `glm-5.2`, `cerebras` → `zai-glm-4.7`; Kimi providers (`fireworks`, `opencode-go`, `moonshot`) → `kimi-k2.7-code`, `kimi-code` → `kimi-for-coding`, `together` → `moonshotai/Kimi-K2.7-Code`; `alibaba-coding-plan` → `qwen3.7-plus`; and Claude-Sonnet defaults (`cloudflare-ai-gateway`, `cursor`, `gitlab-duo`, `kilo`, `opencode-zen`, `vercel-ai-gateway`) → Claude Opus 4.x.\n- Restricted models.dev Azure discovery to OpenAI-family IDs (`gpt-`, `o1`, `o3`, `o4`, `codex`, `chatgpt`), excluding Foundry-hosted third parties (Claude/DeepSeek/Llama/Mistral/Phi) that Azure serves through non-Responses APIs.\n- Detected the Azure OpenAI Responses compat surface (developer role, strict tool mode, strict tool-result pairing) by provider id as well as base URL, so bundled `azure` models whose deployment host is only known at runtime still get the right wire behavior.\n- Renamed the `Qwen3-ASR-Flash` model label to `Qwen3 ASR Flash`\n\n### Fixed\n\n- Fixed tool syntax selection for Gemini-family and Gemma model IDs by routing them to dedicated `gemini` and `gemma` formats instead of generic XML\n- Fixed `zhipu-coding-plan` and `together` shipping no bundled models: their descriptors referenced non-existent models.dev keys (`zhipu-coding-plan`, `together`); pointed them at the real keys (`zhipuai-coding-plan`, `togetherai`) so they bundle their GLM and full catalogs respectively.\n- Folded the `azure-openai-responses` API into the OpenAI Responses thinking-inference branches so Azure reasoning models (o-series, GPT-5, Codex) resolve the discrete effort vocabulary (including `xhigh`) and effort-control mode instead of falling through to generic defaults.\n- Fixed `ollama-cloud` discovery inheriting an unsafe cross-provider `contextWindow`/`maxTokens` when `/api/show` returns no size metadata; it now falls back to the safe 128K context / 8K output caps.\n- Dropped internal Fireworks control-plane resource ids (`accounts/fireworks/{models,routers}/…`) from the bundle; only the public request ids ship.\n\n## [15.13.2] - 2026-06-15\n\n### Added\n\n- Added the `ToolCallSyntax` union and `FALLBACK_TOOL_SYNTAX` constant to `@oh-my-pi/pi-catalog/identity` (re-exported from `@oh-my-pi/pi-ai/grammar`).\n- Added `preferredToolSyntax(modelId)` to `@oh-my-pi/pi-catalog/identity`, resolving a model's native tool-call syntax affinity from its family token (Claude→`anthropic`, GLM→`glm`, Kimi→`kimi`, Qwen→`qwen3`, DeepSeek→`deepseek`, OpenAI/gpt-oss→`harmony`, else the `xml` fallback).\n- Added `flux-1-schnell-fp8` to the Fireworks serverless model catalog\n- Added `gpt-oss-20b` to the Fireworks model catalog\n- Added `qwen3-embedding-8b` to the Fireworks model catalog\n- Added `qwen3-reranker-8b` to the Fireworks model catalog\n- Added `Gemma 4 E2B IT` and `Gemma 4 E4B IT` to the Google model catalog\n- Added `qwen/qwen3-asr-flash` to the Zenmux model catalog\n- Added sparse `supportsTools` model metadata so providers can mark models that require in-band tool-call formatting.\n\n### Changed\n\n- Kept non-tool-capable Fireworks serverless models in discovery results and marked them with `supportsTools: false` for fallback-aware handling\n- Extended `modelFamilyToken(modelId)` to classify Claude/OpenAI ids the structured parser misses (older dated forms such as `claude-3-5-sonnet-20241022` and `gpt-4o`), returning `anthropic`/`openai` instead of an empty token.\n\n## [15.13.1] - 2026-06-15\n\n### Added\n\n- Added `modelFamilyToken(modelId)` to `@oh-my-pi/pi-catalog/identity`: a coarse vendor-lineage token (`anthropic`/`openai`/`gemini`/`kimi`/…) for \"are two models the same family?\" comparisons, backed by `parseKnownModel` canonical-id normalization. Opaque and comparison-only; kind/variant collapsed onto the vendor token ([#2406](https://github.com/can1357/oh-my-pi/issues/2406))\n\n### Changed\n\n- Changed catalog metadata to update a model’s per-token pricing to input 0.09 and output 0.18\n- Changed the same cataloged model’s maximum token limit from 384000 to 65536\n\n### Fixed\n\n- Fixed MiniMax-M3 catalog context for `minimax` and `minimax-cn` to report the documented 1M long-context tier instead of the upstream 512K pricing boundary ([#2576](https://github.com/can1357/oh-my-pi/issues/2576)).\n- Fixed OpenCode Go MiMo catalog metadata so title generation and other tool-enabled calls omit unsupported `tool_choice` instead of triggering provider 400s ([#2509](https://github.com/can1357/oh-my-pi/issues/2509)).\n- Fixed OpenCode Go `kimi-k2.7-code` catalog metadata so resolve-gate requests use automatic tool selection instead of Moonshot-rejected forced `tool_choice` ([#2546](https://github.com/can1357/oh-my-pi/issues/2546)).\n- Fixed Anthropic compat for the `github-copilot` host so `supportsEagerToolInputStreaming` defaults to `false` there, matching the Copilot proxy which rejects the per-tool `eager_input_streaming` field ([#2558](https://github.com/can1357/oh-my-pi/issues/2558)).\n- Scoped vLLM model cache validity to the discovery base URL so changed endpoints refetch immediately, and bounded built-in vLLM discovery requests with a timeout.\n\n## [15.12.6] - 2026-06-14\n\n### Added\n\n- Added GLM-5.2 to the bundled zai (GLM Coding Plan) catalog as the selectable 1M served model.\n\n### Changed\n\n- Pinned zai `glm-5.2` to 1M context during catalog generation so endpoint discovery and older fallbacks cannot regress it to 200k.\n- Replaced the hand-maintained `zhipu-coding-plan` GLM reasoning allowlist and vision regex with a `parseGlmModel` family classifier in `identity/classify.ts` (variant + vision + version), surfaced as `isReasoningGlmModelId` / `isGlmVisionModelId`. Discovery now derives reasoning/vision capability from the GLM family instead of a per-id list, so newly-bumped integers (`glm-5.3`, `glm-6`, …) are covered automatically while `-flash`/`-preview` and the vision `…v` shape stay correctly classified.\n\n## [15.12.4] - 2026-06-13\n\n### Added\n\n- Added bundled Fireworks models `deepseek-v4-flash`, `kimi-k2.7-code`, `minimax-m2.5`, `minimax-m3`, `nemotron-3-ultra-nvfp4`, `qwen3.6-plus`, and `qwen3.7-plus`\n- Changed\n\n### Changed\n\n- Model `contextWindow`/`maxTokens` are now `number | null`; discovery emits `null` when a provider reports no limit, replacing the `222222`/`8888` (`UNK_CONTEXT_WINDOW`/`UNK_MAX_TOKENS`) sentinels (now removed). Bundled `models.json` unknown limits are `null`.\n- Changed the `github-copilot` model context window to `524288` tokens\n- Changed Fireworks model discovery to source the control-plane `List Models` API (`GET /v1/accounts/fireworks/models?filter=supports_serverless=true`) instead of the OpenAI-compatible `/v1/models` inference listing. The inference endpoint returns a sparse, account-specific subset that omits on-demand serverless models (e.g. `kimi-k2.7-code`), so newly published serverless models stayed invisible in the picker until hand-added to the bundled catalog. The control-plane catalog enumerates every serverless model with capability metadata (`supportsServerless`/`supportsTools`/`supportsImageInput`/`contextLength`/`displayName`), paginated and filtered to tool-capable `READY` entries, then merged with bundled/models.dev references — the Kimi K2 max-output clamp and DeepSeek V4 thinking-toggle strip are preserved, and unbundled models default to reasoning so `buildModel` derives the Fireworks effort map. New serverless releases now surface automatically with no catalog edits.\n\n### Fixed\n\n- Filled missing `contextWindow` and `maxTokens` in generated `models.json` for proxy/reseller variants by inheriting limits from canonical-family and segment-reference models\n- Ignored zero-cost `x-ai` subscription entries as reference sources when backfilling limits so inflated values are not propagated\n- Fixed the model cache opening with `PRAGMA journal_mode=WAL` before `PRAGMA busy_timeout`, so concurrent omp startups could crash inside `getDb()` on `SQLITE_BUSY` during WAL recovery instead of waiting through the transient lock. The busy handler is now installed before the first lock-taking statement ([#2421](https://github.com/can1357/oh-my-pi/issues/2421)).\n\n## [15.11.8] - 2026-06-12\n\n### Fixed\n\n- Fixed Antigravity `gemini-3.1-pro --thinking high` failing with `Cloud Code Assist API error (400): Request contains an invalid argument.` — the upstream `gemini-3.1-pro-high` deployment rejects every `streamGenerateContent` request on both CCA endpoints while discovery still advertises it. High effort now routes to `gemini-pro-agent` (the same \"Gemini 3.1 Pro (High)\" model, verified accepting the identical request body), and the model-cache fingerprint version was bumped (`merge-v2` → `merge-v3`) so existing fresh caches refetch discovery and pick up the corrected routing immediately.\n\n## [15.11.7] - 2026-06-12\n\n### Added\n\n- Added effort-tier variant collapsing (`variant-collapse`): providers that expose one logical model as several effort/thinking-suffixed upstream ids (Antigravity CCA `gemini-3.5-flash-extra-low`/`-low`/`gemini-3-flash-agent`, `gemini-3[.1]-pro-low|high`, `claude-*[-thinking]` pairs, `gpt-oss-120b-medium`) collapse into one logical entry carrying per-effort upstream routing in `thinking.effortRouting` (plus `thinking.suppressWhenOff` for Cloud Code Assist ids whose baked server default re-applies when `thinkingConfig` is omitted). Request-time code resolves the outbound id via `resolveWireModelId(model, effort)`; selection, caching, and usage attribution key on the logical id.\n- Added the automatic `X`/`X-thinking` pair rule (`deriveThinkingPairFamilies`): any provider's live bare/thinking twin collapses into the bare id, routing thinking-enabled requests to the `-thinking` backing id (trailing or infix token, so `kimi-k2-thinking-turbo` pairs with `kimi-k2-turbo`). Gated on same api and compatible pricing — all-zero cost rows count as unknown, while twins that both carry real, differing prices remain separate SKUs.\n- Added `collapseBuiltModelVariants` and wired collapsing at every materialization point — Antigravity discovery, the catalog generator, and the model-manager merge — so stale sources (old static beside collapsed dynamic results, mixed cache rows) converge on logical entries instead of unioning raw tier ids back into the catalog.\n- Added `thinking.requiresEffort`, baked for reasoning-only upstreams — Gemini 3.x (levels only, no off), Gemini 2.5 Pro (thinkingBudget floors at 128, rejects 0), OpenAI o-series, MiniMax M2, and thinking-variant SKUs (`*-thinking`/`*-reasoner`/`*-reasoning`, with a negation-aware token grammar so `non-thinking` ids never match). Identity derivation bakes it for new entries and `fillThinkingWireDefaults` backfills explicit/cached metadata; `minimumSupportedEffort` exposes the canonical floor. Pair-collapsed twins drop member flags (their off routes to the bare SKU), while identity re-flags pairs whose logical id is itself mandatory\n\n### Changed\n\n- Changed model display names to drop model-extrinsic decorations: gateway author prefixes (`OpenAI: …`, `Google: …`), `(latest)` alias markers, `(Antigravity)` provider attribution, price tiers (`($$$$)`), and promo/lifecycle tags (`(20% off)`, `(retires …)`). `cleanModelName` is applied in `buildModel` (covers live discovery and stale caches) and as a catalog-generator pass; Antigravity discovery no longer appends `(Antigravity)` to display names. Variant tags that map to distinct wire ids (`(Thinking)`, `(free)`, `(Fast)`, dates, regions) are preserved.\n- Changed the `google-antigravity` default model from `gemini-3-pro-high` to `gemini-3.1-pro`\n- Changed `gemini-2.5-flash-thinking` handling from discovery-denylist to collapsing into `gemini-2.5-flash` (thinking-enabled requests route to the `-thinking` backing id)\n- Bumped the model cache schema to v5 so rows predating effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids) are invalidated\n\n### Fixed\n\n- Fixed catalog generation to apply effort-tier variant collapsing before provider grouping to ensure collapsed model families are consistently materialized without being impacted by in-loop mutation\n- Fixed Kimi K2.6 OpenAI-compatible compat metadata to use a 300s stream watchdog floor, covering Fire Pass router ids as well as public `kimi-k2.6` ids so long reasoning starts do not hit the generic first-event timeout ([#2366](https://github.com/can1357/oh-my-pi/issues/2366)).\n\n## [15.11.4] - 2026-06-12\n\n### Fixed\n\n- Fixed MiniMax M2-family and OpenAI gpt-oss model metadata so OpenAI-compatible catalog entries declare only `low|medium|high` thinking efforts. Their upstreams reject `minimal`, `xhigh`, and Fireworks' `minimal → none` wire mapping, so `fireworks/minimax-m2.7` as the smol auto-thinking classifier model 400ed on every turn. OpenAI-compatible provider effort maps (`Groq qwen/qwen3-32b`, DeepSeek-family, OpenRouter Anthropic adaptive, Fireworks `minimal → none`) now bake into `thinking.effortMap` in catalog metadata instead of `buildOpenAICompat`, and request builders read that field directly. Regenerated `models.json` now makes `disableReasoning` choose `low` for those families while leaving GLM-5.x and other Fireworks models on the existing `minimal → none` path ([#2315](https://github.com/can1357/oh-my-pi/issues/2315)).\n\n### Added\n\n- Added `requiresJuiceZeroHack` Responses-API compat flag, resolved by `buildOpenAIResponsesCompat` from GPT-5-family model names and overridable via sparse model `compat` config. Replaces the request-time `model.name.startsWith(\"gpt-5\")` sniff that gated the trailing `# Juice: 0 !important` no-reasoning developer item.\n\n## [15.11.3] - 2026-06-11\n\n### Added\n\n- Added `requestModelId` on `Model` to represent the upstream model id used when a catalog entry is a local variant\n- Added synthetic GitHub Copilot long-context model variants with `-1m` suffixes when tiered token pricing is advertised\n\n### Changed\n\n- Changed GitHub Copilot discovery to request `X-GitHub-Api-Version: 2026-06-01` from `api.githubcopilot.com`\n- Changed GitHub Copilot discovery to cap base model `contextWindow` to the default token tier and keep long-context access as the separate `-1m` model entry\n- Changed Copilot model mapping to omit non-chat `/models` entries and enable image input for models whose capabilities indicate vision support\n\n### Fixed\n\n- Fixed long-context variant pricing to use `billing.token_prices.long_context` rates instead of default model pricing\n- Fixed `mapModel` handling in OpenAI-compatible discovery so returning `null` now skips a model entry rather than falling back to defaults\n- Fixed model ID precedence so a real upstream Copilot model id is kept when it conflicts with a synthesized `-1m` variant\n\n## [15.11.1] - 2026-06-11\n\n### Fixed\n\n- Fixed NVIDIA NIM Qwen turns failing with `400 Validation: Unsupported parameter(s): enable_thinking`. NIM's chat-completions schema is `additionalProperties: false` and exposes thinking via the vLLM convention `chat_template_kwargs.enable_thinking`; `buildOpenAICompat` was sending top-level `enable_thinking` for every `qwen/*` id regardless of host. Registered `nvidia` as a known host (`integrate.api.nvidia.com`) and routed NVIDIA-hosted Qwen models to `thinkingFormat: \"qwen-chat-template\"` ([#2299](https://github.com/can1357/oh-my-pi/issues/2299)).\n- Fixed Moonshot/Kimi native OpenAI-compatible request metadata so Kimi K2 uses `max_tokens` and omits OpenAI-only `store`, restoring first-turn output with `MOONSHOT_API_KEY` ([#2289](https://github.com/can1357/oh-my-pi/issues/2289)).\n\n## [15.11.0] - 2026-06-10\n\n### Fixed\n\n- Fixed `buildModel` so malformed explicit thinking metadata without `efforts` is treated as sparse input and inferred instead of crashing during model resolution ([#2251](https://github.com/can1357/oh-my-pi/issues/2251)).\n\n## [15.10.12] - 2026-06-10\n\n### Added\n\n- Added `grok-composer-2.5-fast` (Cursor \"Composer 2.5 Fast\") to the xAI Grok OAuth (SuperGrok) catalog: non-reasoning, text-only, 200K context.\n\n### Changed\n\n- Set every xAI Grok OAuth (SuperGrok) curated model's max output tokens to mirror its context window (`grok-build`, `grok-4.3`, `grok-4.20-0309-{reasoning,non-reasoning}`, `grok-4.20-multi-agent-0309`, `grok-composer-2.5-fast`), replacing the `8888` `UNK_MAX_TOKENS` placeholder (and a stale `30000` on three grok-4.x entries). xAI's OAuth `/v1/models` reports no per-request output limit, so the curated catalog now owns `maxTokens` like `contextWindow`, deterministic on both the static-seed and online-overlay paths; the `openai-responses` wire still clamps the actual request to `OPENAI_MAX_OUTPUT_TOKENS` (64k).\n\n### Fixed\n\n- Excluded zero-cost `xai-oauth` subscription entries from the model reference indexes (`buildModelReferenceIndex`, `createReferenceResolver`), so their zero pricing and context-window-sized `maxTokens` cannot outrank paid/public Grok references when resolving custom-provider model identities.\n\n## [15.10.11] - 2026-06-10\n\n### Added\n\n- Added `hostMatchesUrl`, `modelMatchesHost`, and endpoint-shape helpers in the new `hosts` module for consistent provider/baseUrl matching\n- `buildModel(spec)` (`build.ts`) is now the single Model constructor: it materializes the fully-resolved compat record and canonical thinking metadata exactly once (compat first, thinking derived from identity + resolved compat), so `Model.compat` is a required, complete `CompatOf<TApi>` (`ResolvedOpenAICompat`/`ResolvedOpenAIResponsesCompat`/`ResolvedAnthropicCompat`) and request-path code reads fields with zero URL parsing and zero per-request allocation. Sparse user/config overrides live on the new `ModelSpec<TApi>` input shape and survive on `Model.compatConfig` for introspection.\n- Added `ResolvedAnthropicCompat.supportsSamplingParams` (Opus 4.7+/Fable/Mythos reject `temperature`/`top_p`/`top_k` with a 400), baked at build time from model identity so the request path stops re-parsing model ids.\n- Compat detection gained model-time flags so handlers stop sniffing baseUrl: completions `supportsReasoningParams`, `alwaysSendMaxTokens`, `isOpenRouterHost`, `isVercelGatewayHost`, `streamIdleTimeoutMs`, and a precomputed `whenThinking` alternate view (OpenCode `reasoning_content` gating, #1071/#1484); responses `strictResponsesPairing`, `supportsLongPromptCacheRetention`, `supportsReasoningEffort`; anthropic `officialEndpoint`, `requiresToolResultId`, `replayUnsignedThinking`.\n- New `@oh-my-pi/pi-catalog` package: the model catalog extracted from `@oh-my-pi/pi-ai`. Owns the bundled `models.json` and its generation pipeline (`scripts/generate-models.ts`), the core model data types (`Model`, `Api`, `ThinkingConfig`, `Effort`, `Usage`, compat interfaces), thinking metadata enrichment and generated policies (`model-thinking.ts`), the SQLite model cache and model manager, per-provider discovery factories (`provider-models/`), the discovery protocol clients (`discovery/`), and the new `CATALOG_PROVIDERS` table — the single source of truth for provider ids, default models, and discovery wiring (`KnownProvider`, `PROVIDER_DESCRIPTORS`, and `DEFAULT_MODEL_PER_PROVIDER` are derived from it).\n- New `identity/` module centralizing model-identity concerns that were previously duplicated across packages: family classification and version parsing (`identity/classify.ts`, extracted from pi-ai's `model-thinking` internals), canonical model equivalence with injected reference data (`identity/equivalence.ts`, from coding-agent's `model-equivalence`), proxy/reseller reference lookup (`identity/reference.ts`, from coding-agent's `model-registry`), bracket-affix and id-segment helpers (`identity/id.ts`), a single trailing-marker vocabulary with canonical vs reference flavors (`identity/markers.ts` — `search` stays reference-only so Perplexity's `sonar-pro-search` remains canonical-distinct), and provider priority ordering (`identity/priority.ts`).\n- Memoized bundled-reference accessors (`getBundledCanonicalReferenceData` / `getBundledModelReferenceIndex` in `identity/bundled.ts`): one lazy walk of the bundled catalog feeds both canonical equivalence and proxy-reference lookup, so consumers no longer hand-roll the glue.\n- `identity/selection.ts`: pure canonical-variant selection (`resolveCanonicalVariant`, `buildCanonicalModelOrder`, `CanonicalVariantPreferences`) extracted from the coding-agent registry — provider rank, then exact-id match, variant source, id length, and candidate order.\n\n### Changed\n\n- Changed OpenAI compatibility detection to use shared host classifiers (`modelMatchesHost`/`hostMatchesUrl`) with normalized matching instead of raw URL substring checks\n- Changed `hostMatchesUrl`/`modelMatchesHost` usage in compatibility detection to reduce mismatches across case variants and provider alias hosts\n- Provider catalog entries now carry the runtime API-key env fallback as an ordered `envVars` list; `catalogDiscovery.envVars` became an optional generation-time override (only `cursor` and `vercel-ai-gateway` differ) and `PROVIDER_DESCRIPTORS` materializes the resolved list for `generate-models.ts`.\n- `Model`'s api parameter now defaults to `Api` instead of `any` (`Model<TApi extends Api = Api>`), so bare `Model` no longer behaves as `Model<any>` at call sites.\n- `ThinkingConfig` is now explicit and total: an ordered `efforts` array replaces the `minLevel`/`maxLevel`/`levels` range encoding, and the wire facts are baked alongside it — `effortMap` (anthropic-adaptive 4-tier vs 5-tier scale, shared with the OpenRouter completions remap) and `supportsDisplay` (adaptive `display` field support). Explicit spec thinking owns the capability surface (`mode`/`efforts`/`defaultLevel`) and wins over inference; missing wire facts are backfilled from identity so configs never need to know Anthropic's tier tables. Reasoning models that reject the wire effort param (`compat.supportsReasoningEffort: false` on openai-responses*) are encoded as `thinking: undefined` (\"thinks, no control surface\") instead of the removed `modelOmitsReasoningEffort` special case. `models.json` was re-baked in the new vocabulary behind a 3196-model behavioral parity gate, and the model cache schema bumped to v4 to invalidate old-shape rows.\n- `mapEffortToGoogleThinkingLevel(effort)` is now a static map (model parameter dropped — validation stays at the `requireSupportedEffort` call sites), and `mapEffortToAnthropicAdaptiveEffort` reads the baked `thinking.effortMap` instead of re-classifying the model id per request.\n- Generator-only policy code moved out of the runtime bundle into `scripts/generated-policies.ts`: `applyGeneratedModelPolicies` (now policy fixups + thinking re-bake via the shared deriver), `linkOpenAIPromotionTargets`, the Copilot context-window table, minimax/opencode-go compat fixups, and `CLOUDFLARE_FALLBACK_MODEL`. The anthropic id predicates (`hasOpus47ApiRestrictions`, `supportsMidConversationSystemMessages`, `isAnthropicFableOrMythosModel`) moved to `identity/family` for build-time use by the compat/thinking derivers only.\n\n### Fixed\n\n- Fixed Anthropic official-endpoint detection to require strict HTTPS hostname matching so non-official or lookalike URLs are no longer treated as official Anthropic hosts\n- Fixed Ollama Cloud dynamic discovery so same-id matches from other providers no longer supply context-window or max-output-token limits for discovered models.\n- Wired `@oh-my-pi/pi-catalog` into the release publish package list, tarball install smoke test, and root `bun generate-models` script.\n- Fixed `supportsAdaptiveThinkingDisplay` only matching dash-form version ids: dotted ids (`claude-opus-4.7`) now classify through `identity/classify` like every other anthropic predicate, so six bundled dotted Opus 4.7/4.8 entries (github-copilot, vercel-ai-gateway, zenmux) regain adaptive `display` support; bare dated ids (`claude-opus-4-20250514` = Opus 4.0) stay excluded.\n- Fixed the OpenRouter anthropic adaptive-effort map misclassifying bare dated Opus ids (`claude-opus-4-20250514` parsed as version 4.20 → wrongly adaptive); the map now derives from the shared classifier and the shared 4-/5-tier tables.\n\n### Removed\n\n- Removed the runtime enrichment layer: `enrichModelThinking` (and its non-enumerable memo-slot cache), `refreshModelThinking`, `modelOmitsReasoningEffort`, and the `model-thinking` re-exports of generator-only policies. Thinking metadata is resolved exactly once inside `buildModel`; runtime helpers (`getSupportedEfforts`, `clampThinkingLevelForModel`, `requireSupportedEffort`, the effort mappers) are pure field reads.\n"}, "06343fef4200c4e32d18f08df5a6a8bd84dcc710": {"t": 1785674218.5296907, "n": 80122, "h": "69dc0add4bf7c330", "c": "# Changelog\n\n## [Unreleased]\n\n## [17.2.4] - 2026-08-01\n\n### Added\n\n- Added `AnthropicCompat.streamIdleTimeoutMs` and propagated it through `buildAnthropicCompat` so direct Anthropic provider streams can configure their inter-event idle watchdog.\n- Fixed Ollama Cloud DeepSeek V4 Pro/Flash models (including dated tag variants such as `deepseek-v4-flash:0731`) reporting an incorrect max-output-tokens figure by pinning it to the deployment's enforced 65536-token output ceiling ([#7266](https://github.com/can1357/oh-my-pi/issues/7266)).\n\n### Fixed\n\n- Fixed `gen:models` Codex discovery to union models across every stored OAuth account and fail closed on partial resolution, matching runtime discovery ([#6265](https://github.com/can1357/oh-my-pi/issues/6265)); restored the bundled `gpt-5.4`, `gpt-5.6-sol`, and `gpt-5.3-codex-spark` entries a single-account regen had dropped.\n- Fixed `google-antigravity` models always reporting $0 cost: Antigravity discovery carries no pricing, so the generator now back-fills each model with its Google list price (Gemini ids from the `google` provider, including `-preview` id aliases; Claude ids from `google-vertex`, falling back to `anthropic`).\n- Fixed OpenRouter `deepseek/deepseek-v4-flash-0731` exposing only `high` thinking effort by consuming the live `reasoning.supported_efforts` and `default_effort` metadata and bundling its `low`/`high`/`max` ladder. ([#7307](https://github.com/can1357/oh-my-pi/issues/7307))\n\n## [17.2.3] - 2026-08-01\n\n### Added\n\n- Added support for the ai& provider (`aiand`), an OpenAI-compatible inference API with dynamic model discovery (context windows, capabilities, reasoning efforts, and USD pricing from `/v1/models`) and API-key authentication via the `AIAND_API_KEY` environment variable.\n\n## [17.2.2] - 2026-07-31\n\n### Added\n\n- Added support for the GMI Cloud provider (`gmi-cloud`), an OpenAI-compatible inference gateway with dynamic model discovery and API-key authentication via the `GMI_API_KEY` environment variable.\n- Added optional authoritative context occupancy to usage records for providers with separate checkpoint telemetry and billable token buckets.\n\n### Fixed\n\n- Fixed classification of dynamically discovered Cursor Kimi K3 effort variants as non-reasoning models when `thinkingDetails` is omitted.\n- Fixed Google AI Studio OpenAI-compatible requests failing with HTTP 400 by omitting the unsupported `store` field.\n- Fixed Synthetic models losing capabilities (such as reasoning/thinking selectors, vision input, output limits, and pricing) by correcting how the discovery mapper parses Synthetic's advertised features, effort vocabularies, and pricing structures.\n- Fixed Cursor model discovery to correctly expose the 1M-token context window for supported models (including Claude, GPT, Kimi K3, and GLM 5.2+ families) instead of defaulting to 200k.\n- Fixed GitHub Copilot routing for `grok-4.5` to use the correct Responses endpoint instead of the unsupported Chat Completions endpoint.\n\n## [17.2.1] - 2026-07-30\n\n### Fixed\n\n- Fixed Ollama model-manager caches being reused after the configured base URL changed by scoping cache namespaces to the normalized native discovery endpoint, including reverse-proxy path prefixes ([#7087](https://github.com/can1357/oh-my-pi/issues/7087)).\n\n## [17.2.0] - 2026-07-30\n\n### Added\n\n- Regenerated the Cursor agent protobufs (`discovery/cursor-gen/agent_pb.ts`) against the modern `agent.proto`, adding the message and enum families current Cursor CLI builds emit: Pi tool exec frames, hook queries and responses, subagents, allowlist prechecks, MCP state, smart-mode classification, canvas diagnostics, conversation search, agent-store conflicts and git diff. Purely additive — no existing exported symbol changed shape.\n\n### Fixed\n\n- Fixed an issue where LM Studio first turns failed with a 400 Invalid tool_choice error when a named tool was forced, by using the supported tool_choice: \"required\" string selector.\n\n## [17.1.8] - 2026-07-28\n\n### Added\n\n- Added `resolveVertexEndpointHost(location)` utility to resolve the correct Vertex AI API endpoint hostnames for global, multi-region, and regional locations.\n\n### Fixed\n\n- Fixed an issue where `calculateCost` under-reported Anthropic cache-write costs by honoring the `usage.cttl` breakdown to correctly price 1-hour retention writes at 2x the base input rate.\n\n## [17.1.7] - 2026-07-27\n\n### Added\n\n- Added support for moonshotai/Kimi-K3 and kimi-k3-fast models\n- Added umans-kimi-k3 prerelease model configuration\n\n### Changed\n\n- Updated pricing and token limits for selected models\n\n## [17.1.6] - 2026-07-27\n\n### Added\n\n- Added SiliconFlow providers (`siliconflow`, `siliconflow-cn`) with dynamic-only OpenAI-compatible model discovery: no bundled catalog — the model list is fetched live from each region's `/v1/models` endpoint, with non-chat entries (embedding, reranker, image, audio, video) filtered out. Discovery hydrates pricing, context/output limits, and reasoning metadata from the provider's models.dev catalog at runtime (with bundled upstream references as a reasoning-only fallback for ids models.dev has not indexed), so reasoning models keep thinking enabled and sessions compact against real context windows. `SILICONFLOW_API_KEY` / `SILICONFLOW_CN_API_KEY` environment variables are wired into `getEnvApiKey`.\n\n## [17.1.5] - 2026-07-27\n\n### Fixed\n\n- Fixed Kimi Code (`kimi-code`) reporting `maxTokens: 32000` for every model — its `/coding/v1/models` discovery mapper and the bundled catalog applied a blanket constant, truncating `k3`/`k3-256k` output at ~4x below their real 131072 ceiling and `kimi-for-coding`/`kimi-for-coding-highspeed` below their 32768 ceiling. Output caps are now derived per family, and the model cache is invalidated so upgrades drop the stale `maxTokens: 32000` rows (including the discovery-only `k3-256k`) instead of serving them until the next network refresh ([#6711](https://github.com/can1357/oh-my-pi/issues/6711)).\n- Fixed Anthropic model discovery 404ing when the registry derived the provider base URL from a bundled model without the `/v1` suffix (`https://api.anthropic.com/models` instead of `/v1/models`), which let a stale text-only cache row shadow fresh models.dev vision metadata — surfacing as snapcompact refusing to run on `claude-opus-5`. Discovery now always targets `/v1/models` while model rows keep the provider base URL ([#6563](https://github.com/can1357/oh-my-pi/issues/6563)).\n\n## [17.1.4] - 2026-07-26\n\n### Added\n\n- Added Claude Opus 5 model entries for Amazon Bedrock: `anthropic.claude-opus-5` plus its `us.`, `eu.`, `au.`, and `global.` regional/geo IDs.\n\n### Fixed\n\n- Fixed `alibaba-token-plan` locking out China (Beijing) 百炼 Token Plan subscribers: the provider hardcoded the international Singapore endpoint, so Beijing-issued `sk-sp-` keys got `401 invalid_api_key`. The wire credential now carries an optional region base URL, and model discovery targets the credential's region ([#6682](https://github.com/can1357/oh-my-pi/issues/6682)).\n- Fixed forced `tool_choice` 400s (`tool_choice 'specified' is incompatible with thinking enabled`) on Kimi Code's Anthropic-compatible endpoint for the `kimi-for-coding`, `kimi-for-coding-highspeed`, and `k3` aliases: the Anthropic-surface compat matcher only recognised Moonshot's native `kimi-k2.7-code*` ids, so thinking-locked kimi-code models kept `supportsForcedToolChoice: true` and the forced selector was sent to a host that always thinks. These models now resolve `requiresThinkingEnabled`, keeping thinking on and downgrading forced choices to `auto`.\n- Retried empty successful provider discovery responses after the short non-authoritative interval instead of caching them for the full catalog TTL ([#6620](https://github.com/can1357/oh-my-pi/issues/6620)).\n- Fixed GitHub Copilot Claude models with no bundled catalog reference (e.g. a freshly served `claude-opus-5`) discovering with `reasoning: false`/`thinking: null` and no effort dial, and disappearing along with their synthesized `-1m` sibling on offline reads: reference-less Copilot models on the anthropic-messages proxy now derive the adaptive reasoning ladder from the model id, and the cache restores their compile-time `COPILOT_API_HEADERS` by value instead of dropping them as unrestorable ([#6664](https://github.com/can1357/oh-my-pi/issues/6664)).\n\n## [17.1.3] - 2026-07-24\n\n### Fixed\n\n- Disabled the first-event watchdog for local OpenAI-compatible backends while retaining the 300-second inter-event watchdog, so long llama.cpp prompt prefill is not canceled and retried ([#6524](https://github.com/can1357/oh-my-pi/issues/6524)).\n\n## [17.1.1] - 2026-07-24\n\n### Added\n\n- Added catalog metadata for models that support native computer-use requests.\n- Added resolved Bedrock Converse prompt-cache compatibility limits, including explicit 5-minute checkpoint support for bundled Nova Lite, Micro, Pro, Premier, and Nova 2 Lite models plus their documented in-region, regional, and global IDs, and model-specific 1-hour Claude retention.\n- Added resolved Bedrock Converse prompt-cache compatibility limits, including explicit 5-minute checkpoint support for bundled Nova Lite, Micro, Pro, and Premier models plus Nova Premier's documented in-region model ID, and model-specific 1-hour Claude retention.\n- Added the native Meta Model API provider and Muse Spark 1.1 with Responses API reasoning replay, image input, and the full supported reasoning-effort ladder ([#4941](https://github.com/can1357/oh-my-pi/issues/4941)).\n- Added an opt-in Vercel AI Gateway automatic prompt-cache compatibility option alongside provider routing preferences.\n- Added Vercel AI Gateway Responses cache-anchor and cache-lifetime compatibility controls.\n- Added resolved OpenAI GPT-5.6 prompt-cache breakpoint capability metadata, keeping older models and compatible endpoints opt-in only.\n- Added the native `alibaba-token-plan` provider with QwenCloud Token Plan Individual discovery and a curated chat-model fallback catalog ([#6151](https://github.com/can1357/oh-my-pi/issues/6151)).\n\n## [17.1.0] - 2026-07-24\n\n### Added\n\n- Added Bedrock Converse prompt-cache compatibility limits, including 5-minute checkpoint support for Nova models (Lite, Micro, Pro, Premier, Nova 2 Lite) and 1-hour Claude retention.\n- Added native Meta Model API provider and Muse Spark 1.1 support, featuring Responses API reasoning replay, image input, and reasoning-effort controls.\n- Added Vercel AI Gateway integration features, including opt-in automatic prompt-cache compatibility, provider routing preferences, and Responses cache-anchor and cache-lifetime controls.\n- Added prompt-cache breakpoint capability metadata for OpenAI GPT-5.6, with opt-in support for older models and compatible endpoints.\n- Added native alibaba-token-plan provider with QwenCloud Token Plan Individual discovery and a curated chat-model fallback catalog.\n\n## [17.0.9] - 2026-07-23\n\n### Changed\n\n- Renamed `codex-auto-review` model to `GPT-5.3 Codex Spark` with updated pricing and capabilities\n- Removed image input support from GPT-5.3 Codex Spark (text-only)\n- Reduced GPT-5.3 Codex Spark context window from 272K to 128K tokens\n- Changed GPT-5.3 Codex Spark thinking efforts from `[\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"]` to `[\"low\", \"medium\", \"high\", \"xhigh\"]`\n- Updated pricing for multiple AI models across providers (costs adjusted in models.json)\n- Reduced max output tokens for an unspecified model from 16384 to 8192\n- Added image input support to Venice AI text model\n\n## [17.0.8] - 2026-07-22\n\n### Added\n\n- Added support for several new models across multiple providers, including MiniMax M3, Gemini 3.5 Flash Lite, Gemini 3.6 Flash (with thinking support), Hy3, Doubao-Seed-Character, LongCat 2.0, Laguna S 2.1 (free and paid tiers), Qwen 3.6 35B A3B, SWE-1.6 Slow (devin agent catalog), and XiaomiMiMo/MiMo-V2.5.\n\n### Changed\n\n- Updated Grok 4.5 API type to \"openai-responses\" and updated \"o3-mini\" to support thinking capabilities with the \"kimi\" thinking format.\n- Renamed OpenRouter-specific models and routers to include \"OpenRouter\" in their names (e.g., \"OpenRouter Auto Router (Beta)\", \"OpenRouter Body Builder (beta)\", and \"OpenRouter Pareto Code Router\").\n- Updated context window sizes, costs, and token limits for numerous models.\n\n### Fixed\n\n- Fixed an issue where GPT-5.6 Codex SKUs lost usable context window capacity due to dynamic discovery values overwriting bundled limits.\n- Fixed OpenAI Codex discovery dropping account-listed ChatGPT-only models (such as GPT-5.3 Codex Spark) when they are unavailable through the public API.\n- Fixed Codex catalog discovery hiding models when multiple OAuth accounts are configured by independently fetching and merging catalogs from all accounts.\n- Fixed cached models reusing a bundled request model (such as GitHub Copilot long-context variants) being incorrectly flagged as unrestorable and dropped after a restart.\n- Fixed LM Studio discovery reporting a model's theoretical maximum context length instead of the actual loaded context window size of the running instance.\n\n### Removed\n\n- Removed several deprecated model families from the devin catalog, including Claude Fable 5, Claude Opus 4.6/4.7, Claude Sonnet 4.6/5, DeepSeek V4 Pro, Gemini 3.1 Pro, Gemini 3.5 Flash, GLM-5.2, SWE-1.6, and Nemotron 3 Ultra.\n- Removed GPT-5 through GPT-5.3 Codex variants and GPT-5.4 nano from the openai-codex catalog.\n\n## [17.0.6] - 2026-07-20\n\n### Added\n\n- Added static fallback seed for Devin's `swe-1-7` model so it is bundled even when catalog generation runs without a Devin session token.\n\n### Fixed\n\n- Collapsed Devin's six GLM-5.2 variants into two logical entries (`glm-5-2` for 200K free, `glm-5-2-1m` for 1M paid). The 200K entry routes every thinking effort to the free `glm-5-2` wire UID — never to the quota-gated `glm-5-2-max` or `glm-5-2-none` — so GLM-5.2 works even when the weekly usage quota is exhausted.\n\n## [17.0.5] - 2026-07-18\n\n### Added\n\n- Added an Anthropic compatibility flag to allow non-official OAuth endpoints to opt into configured Claude Code fingerprint header overrides.\n\n### Fixed\n\n- Fixed a security issue where sensitive provider-defined request headers (such as API keys or credentials) were serialized in plaintext within the model cache (models.db). The cache now omits these headers, securely invalidates older cached rows, and restores or refetches them dynamically.\n- Fixed OpenAI Codex discovery to respect caller-supplied fetch configurations (such as proxies or custom CAs) and correctly replace stale bundled models with the authenticated account catalog.\n- Fixed stream timeouts and retry loops during long prefills on local loopback or RFC1918 backends (such as litellm proxies fronting local servers) by applying the local stream-timeout floor to these backends.\n- Fixed Kimi K3 models served through generic OpenAI-compatible routes exposing unsupported reasoning efforts instead of the mandatory low/high/max scale.\n\n## [17.0.4] - 2026-07-18\n\n### Changed\n\n- Kimi-family models now use MFJS tool schema on all hosts, including proxies like OpenRouter that forward schemas to Moonshot\n\n## [17.0.3] - 2026-07-17\n\n### Fixed\n\n- Logged LiteLLM rich-metadata endpoint failures once with their endpoint and status before falling back to incomplete `/v1/models` data ([#5801](https://github.com/can1357/oh-my-pi/issues/5801)).\n- Fixed authenticated Kimi Code discovery to preserve live effort levels, default effort, mandatory-thinking state, and per-model protocol metadata ([#5893](https://github.com/can1357/oh-my-pi/issues/5893)).\n- Fixed LiteLLM provider ignoring per-model pricing: `mapLiteLLMRichEntry` now reads `input_cost_per_token` / `output_cost_per_token` (plus cache costs) from LiteLLM rich metadata and maps them to `cost.input` / `cost.output`, falling back to the bundled reference only when LiteLLM omits cost, so proxied models no longer display as free ([#5818](https://github.com/can1357/oh-my-pi/issues/5818)).\n\n## [17.0.2] - 2026-07-17\n\n### Changed\n\n- Increased the maximum output tokens (maxTokens) from 32,768 to 65,536 for Kimi K2.7-Code models on Fireworks.\n\n### Fixed\n\n- Fixed a regression where the context window for openai-codex GPT-5.6 models (Luna, Sol, Terra) incorrectly fell back to 272,000 instead of preserving its 372,000 capacity.\n- Fixed Umans PAYG models incorrectly displaying as \"Free\" in /models by correctly sourcing their published per-token rates.\n- Fixed native moonshot/kimi-k3 capabilities and pricing, ensuring it correctly reflects its official pricing, 1M context window, image input support, reasoning capabilities, and 128k output token limit.\n\n## [17.0.1] - 2026-07-16\n\n### Added\n\n- Added GPT-5.6 Luna, Sol, and Terra entries for Amazon Bedrock, Azure, and Cloudflare\n- Added KAT-Coder Air/Pro V2.5 entries across Kilo, OpenRouter, NanoGPT, and Vercel\n- Added Inkling model entries for Baseten and Vercel AI Gateway\n- Added Umans DeepSeek V4 Pro DSpark as an experimental model listing\n- Added Claude Opus 4.7 Fast and 4.8 Fast on Vercel AI Gateway\n- Added Workers AI GLM-5.2, Muse Spark 1.1, Stealth GPT-5.6 Sol, and nano-gpt-help entries\n\n### Changed\n\n- Added image input and reasoning support to several existing Codeium and Kilo GPT-5.6 models\n- Enabled image input and reasoning for Gemini Flash Latest and Grok 4.5\n- Renamed many model labels for consistency, including Claude, Grok, DeepSeek, GLM, and Gemi­ni names\n- Updated pricing for many existing models, including input, output, and cache cost values\n- Updated context window and max token limits for many catalog models across providers\n\n### Fixed\n\n- Fixed Z.AI (GLM) coding-plan token costs all showing as \"Free\" in `/models`: the `zai` provider descriptor sourced the models.dev `zai-coding-plan` key (all-$0 subscription rates) instead of the `zai` pay-as-you-go key, which carries the real per-token rates for the identical GLM ids ([#5598](https://github.com/can1357/oh-my-pi/issues/5598)).\n- Fixed custom Anthropic endpoints receiving the first-party-only `eager_input_streaming` tool field by default ([#5572](https://github.com/can1357/oh-my-pi/issues/5572)).\n- Added resolved OpenAI sampling-parameter compatibility metadata for o-series and GPT-5+ models.\n- Fixed GitHub Copilot `mai-code-1-flash-picker` (and other `mai-*` models) to route through the `/responses` endpoint instead of `/chat/completions`, which rejected them with `400 unsupported_api_for_model` ([#5612](https://github.com/can1357/oh-my-pi/issues/5612)).\n- Extended the reasoning `streamIdleTimeoutMs` floor (300s) to native Kimi K2.7 Code (`kimi-k2.7-code` / `kimi-k2.7-code-highspeed`), which previously fell through to the 120s default and aborted on long reasoning turns ([#4836](https://github.com/can1357/oh-my-pi/issues/4836)).\n- Fixed GLM-5.x coding-plan streams via the OpenCode Go/Zen gateways (`opencode.ai/zen/…`) timing out with `OpenAI completions stream stalled while waiting for the next event` during slow plan-writing/reasoning phases. The 600s idle-timeout floor for GLM coding-plan SKUs was gated to the native Z.AI/Zhipu hosts only, so OpenCode-fronted GLM fell back to the 120s default watchdog. ([#4758](https://github.com/can1357/oh-my-pi/issues/4758))\n\n## [16.5.2] - 2026-07-14\n\n### Fixed\n\n- Fixed OpenCode Zen and Go discovery to replace stale bundled models with each provider's live model catalog.\n\n## [16.5.1] - 2026-07-14\n\n### Fixed\n\n- Fixed reasoning effort mapping for Z.ai GLM-5.2 on the Anthropic messages endpoint to correctly use the two-tier scale (high, max) and emit output_config.effort.\n- Fixed an issue where stale cached model limits would override updated static catalog limits after a catalog fingerprint mismatch.\n- Fixed Cursor discovery to correctly preserve GetUsableModels max-mode metadata for premium models and invalidate stale cache entries.\n\n## [16.4.3] - 2026-07-11\n\n### Fixed\n\n- Fixed parsing of SAP AI Core Claude model IDs in version-first format (e.g., anthropic--claude-4.8-opus), restoring adaptive thinking metadata and capability gates.\n- Fixed GitHub Copilot Business and Enterprise model discovery to correctly preserve vision capabilities instead of downgrading models to text-only.\n\n## [16.4.2] - 2026-07-10\n\n### Fixed\n\n- Fixed OpenAI Codex model discovery to include the Codex version header alongside the client_version query parameter.\n\n## [16.4.1] - 2026-07-10\n\n### Added\n\n- Added GPT-5.6 Luna, Sol, and Terra models\n- Added perplexity-academic-researcher model\n\n### Changed\n\n- Updated context windows for multiple GPT-5.6 models\n- Increased max tokens for several models\n- Updated cache write costs for GPT-5.6 variants\n- Reduced pricing for select models\n\n### Removed\n\n- Removed the generated GPT-5.6 pro-reasoning aliases (`gpt-5.6-{luna,sol,terra}-pro`) from the `openai-codex` subscription provider — pro reasoning is not offered on subscriptions; the `openai` API-key aliases remain\n\n## [16.4.0] - 2026-07-10\n\n### Breaking Changes\n\n- Redesigned reasoning effort ladders to be wire-exact, removing the shifted five-tier effort mapping. Models now expose exactly the effort tiers their upstream APIs accept, mapped 1:1. Removed SHIFTED_FIVE_TIER_EFFORT_MAP, ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER, and per-host xhigh-to-max alias maps. Selecting an unsupported tier now automatically clamps down via clampThinkingLevelForModel. Devin effort routing is now mapped 1:1 onto per-tier siblings.\n\n### Added\n\n- Added support for new models: Grok 4.5 family, Dolphin Mistral 24b Venice Edition, GLM5.2-Fast, and Zenmux variants for GPT-5.6 (Luna, Sol, and Terra).\n- Added Novita as a model provider, including public catalog discovery, pricing, limits, modality, reasoning, and tool metadata.\n- Added useResponsesLite to Model and ModelSpec to support the Responses Lite transport, enabled by default for the GPT-5.6 family.\n- Added Effort.Max (\"max\") as a first-class user-facing thinking level above xhigh.\n\n### Changed\n\n- Enabled reasoning effort controls for Grok 4.5 and updated support flags for additional Grok variants\n- Standardized reasoning effort levels to use a wire-exact max tier across all model providers, including Devin routing and Ollama configurations.\n- Updated costs and context windows for various models in the catalog.\n\n## [16.3.15] - 2026-07-09\n\n### Added\n\n- Added support for Grok 4.5 model\n- Added `gpt-5.6` base models and `gpt-5.6-{luna,sol,terra}-pro` variants\n- Added `meta/muse-spark-1.1` model support\n- Added support for thinking modes on `poolside/laguna` models\n- Added generated GPT-5.6 Pro aliases (`gpt-5.6-{luna,sol,terra}-pro`) on the `openai` and `openai-codex` providers: each alias sends the base model id on the wire (`requestModelId`) with the new `reasoningMode: \"pro\"` marker, and re-derives from the current base rows on every catalog regeneration.\n\n### Changed\n\n- Updated cache read costs for Grok models\n- Reduced max token limit for Grok 4.3 model\n- Enabled prompt cache affinity for Grok models via the x-grok-conv-id header in OpenAI compatible endpoints\n- Enabled prompt cache affinity for Grok models via the x-grok-conv-id header\n- Marked direct xAI Grok Chat Completions models for `x-grok-conv-id` prompt-cache affinity.\n\n## [16.3.14] - 2026-07-09\n\n### Added\n\n- Added support for GPT-5.6 (Luna, Sol, Terra) model variants\n- Enabled expanded five-tier reasoning effort scale (minimal to xhigh) for GPT-5.6 models\n- Added GPT-5.6 (Terra/Luna/Sol) support for the new `max` reasoning tier: on wire-effort APIs (OpenAI Responses, Codex, Azure, openai-compat/OpenRouter models that advertise reasoning) user efforts shift up one notch — `xhigh` sends `max`, `high` sends `xhigh` — mirroring the Claude Fable/Opus 4.7+ five-tier mapping, and the exposed ladder becomes `minimal..xhigh` with `minimal` reaching the native `low` tier. Devin's per-tier GPT-5.6 sibling rows now collapse into `gpt-5-6-{luna,sol,terra}` logical models with the same shifted routing (`xhigh` → `-max`), plus `-fast` families that keep the direct `low..xhigh` `-priority` scale since Devin serves no `-max-priority` tier.\n\n## [16.3.13] - 2026-07-09\n\n### Added\n\n- Added support for Grok 4.5 across multiple providers\n- Added support for GPT-5.6 series models (Luna, Sol, Terra)\n- Added Aion 3.0 and 3.0 Mini models\n- Added Kuaishou KAT-Coder v2.5 models\n- Added Nex-N2-Mini and SWE-1.7 series models\n- Added Hy3 models and free variants\n\n### Changed\n\n- Updated cost and token configurations for various models across providers\n- Renamed several models for consistency (e.g., MiniMax M3, Gemma 4 31B, Qwen variants)\n\n## [16.3.12] - 2026-07-08\n\n### Fixed\n\n- Fixed LiteLLM discovery stopping at `/model_group/info` when that endpoint omitted `supports_vision`; it now continues to `/model/info` and preserves `model_info.supports_vision=true` for vision-capable proxy models. ([#4747](https://github.com/can1357/oh-my-pi/issues/4747))\n- Fixed LiteLLM discovery to fall back to bundled catalog metadata when `models.dev` lacks a model reference, preserving reasoning and thinking support for models such as `glm-5.2`. ([#4695](https://github.com/can1357/oh-my-pi/issues/4695))\n- Detected Azure AI Inference / Foundry Anthropic routes as strict-tool-incompatible so resolved Anthropic compat disables strict tools before request construction ([#4679](https://github.com/can1357/oh-my-pi/issues/4679)).\n\n## [16.3.11] - 2026-07-06\n\n### Added\n\n- Added Claude Haiku 4.5 (JP) model support\n- Added tencent/hy3 model support via ZenMux\n\n### Changed\n\n- Updated naming format for various synthetic models to include provider prefix\n- Adjusted context window limit for MiniMax-M3 model\n- Updated pricing for select models\n\n## [16.3.10] - 2026-07-06\n\n### Fixed\n\n- Fixed LiteLLM rich discovery to ignore unusable sentinel placeholders and continue to `/v2/model/info` for real models. ([#4655](https://github.com/can1357/oh-my-pi/issues/4655))\n\n## [16.3.9] - 2026-07-06\n\n### Fixed\n\n- Fixed compatibility with OpenCode Go DeepSeek V4 models by sending max_tokens instead of max_completion_tokens to match the provider's API requirements.\n\n## [16.3.7] - 2026-07-05\n\n### Fixed\n\n- Fixed usage cost calculation to correctly account for provider orchestration token sidecars without misclassifying them as standard input, output, or cache tokens.\n\n## [16.3.4] - 2026-07-03\n\n### Added\n\n- Added Baseten as a supported model provider\n- Added support for new models from Baseten, including DeepSeek V4 Pro and Kimi series\n- Added new Devin agent models: Claude 5 Fable variants\n- Added new Github Copilot models: Kimi K2.7 Code and MAI-Code-1-Flash\n- Added Poolside Laguna XS 2.1 models via Kilo and OpenRouter providers\n- Added support for Claude Fable 5 (Free) via Zenmux provider\n\n### Changed\n\n- Updated priority ordering to include Baseten\n- Updated pricing and limits for various existing models in the catalog\n\n## [16.3.3] - 2026-07-02\n\n### Fixed\n\n- Extended Anthropic-compatible signing-endpoint recognition to Cloudflare AI Gateway, Google Vertex, AWS Bedrock, and Azure AI Inference / Foundry to ensure consistent reasoning-replay and signature-stripping behavior, and exposed ResolvedAnthropicCompat.signingEndpoint in the public API.\n- Fixed Zhipu Coding Plan runtime discovery to prioritize account-scoped model lists over bundled fallback models, preventing routing errors for valid non-z.ai keys.\n\n## [16.3.2] - 2026-07-02\n\n### Fixed\n\n- Fixed ZenMux model discovery to run without a `ZENMUX_API_KEY`, so newly published ZenMux models (for example `anthropic/claude-fable-5-free`) auto-update into the runtime `models.db` cache instead of waiting on a regenerated `models.json`.\n- Fixed ZenMux runtime discovery to query the `/api/v1/models` endpoint even when the resolved provider base URL points at the Anthropic-compatible route, so discovery no longer requests a non-existent `/api/anthropic/models` path.\n\n## [16.3.1] - 2026-07-02\n\n### Removed\n\n- Removed reasoning suppression prompt logic for GPT-5 models\n\n## [16.3.0] - 2026-07-02\n\n### Breaking Changes\n\n- Renamed the `requiresJuiceZeroHack` compatibility flag to `requiresReasoningSuppressionPrompt` (affecting `OpenAICompat` and `ResolvedOpenAIResponsesCompat`) and removed the unused `\"juice-zero-developer-message\"` member from `OpenAIReasoningDisableMode`.\n\n### Fixed\n\n- Fixed stream markup healing pattern misfires by disabling the healer on the official OpenAI endpoint.\n- Updated the Xiaomi provider's default model to the supported `mimo-v2.5` model.\n- Fixed model discovery probes (including Ollama and metadata fetches) failing behind private-CA gateways by ensuring they honor the `NODE_EXTRA_CA_CERTS` environment variable.\n- Fixed CoreWeave Serverless Inference project-header detection to ensure blank OpenAI-Project overrides do not block the `COREWEAVE_PROJECT` fallback.\n- Fixed LiteLLM MiniMax M3 discovery to remove reseller-only display suffixes and invalidated the model cache to clear stale suffixes immediately.\n- Fixed ZenMux's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`), matching the GitHub Copilot fix (#2851). ZenMux's `zenmux.ai/api/anthropic` route forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: \"\"` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused `400 messages.1.content.0: Invalid signature in thinking` on Claude Sonnet 5 and other reasoning models. ([#4192](https://github.com/can1357/oh-my-pi/issues/4192))\n\n## [16.2.13] - 2026-07-01\n\n### Added\n\n- Added support for human-readable reasoning summaries on compatible OpenAI Codex models (v5.4+)\n\n### Fixed\n\n- Fixed discovered OpenAI Codex models to advertise V2 streaming remote compaction, avoiding the legacy compact endpoint timeout path for Codex sessions. ([#4146](https://github.com/can1357/oh-my-pi/issues/4146))\n\n## [16.2.12] - 2026-07-01\n\n### Breaking Changes\n\n- Removed runtime canonical-equivalence APIs from the identity module, including resolveCanonicalVariant, buildCanonicalModelOrder, CanonicalVariantPreferences, and getBundledCanonicalReferenceData. These utilities have been transitioned to a build-time generator script and are no longer exposed in the runtime bundle.\n\n## [16.2.11] - 2026-07-01\n\n### Fixed\n\n- Fixed a potential memory leak caused by dangling timeout timers during model discovery in OpenAI-compatible, vLLM, LiteLLM, and LM Studio catalogs.\n- Widened stream watchdogs for local OpenAI-compatible backends (including llama.cpp, LM Studio, vLLM, and Ollama) to prevent premature timeouts during cold model loads.\n\n## [16.2.10] - 2026-06-30\n\n### Added\n\n- Added Claude Sonnet 3.7, Claude Opus 3, and Claude Sonnet 3 model entries to the Anthropic catalog\n- Added Anthropic Claude Sonnet 5 model entry to the Kilo provider catalog\n- Added first-party catalog discovery support for the Anthropic provider\n- Added Gemini 3.1 Flash Lite Image model entry to the Kilo provider catalog\n- Added Anthropic Claude Sonnet 5 model variants with low, medium, high, xhigh, and max thinking efforts to the Devin provider catalog\n- Added Claude Sonnet 5 model entry to the Anthropic curated catalog.\n\n### Changed\n\n- Updated the base API URL for the Claude Sonnet 5 model in the Anthropic catalog\n- Updated pricing metrics for DeepSeek R1 and DeepSeek V3 model entries to reflect new rates\n\n## [16.2.9] - 2026-06-30\n\n### Added\n\n- Added full capability support for Claude Sonnet 5, aligning it with Claude Opus 4.8 and Fable 5. This includes adaptive thinking display, mid-conversation system messages, sampling parameter and thinking omission API restrictions, and 5-tier adaptive reasoning effort mapping (including xhigh and max levels) across direct APIs, OpenRouter, and Bedrock Converse.\n\n### Changed\n\n- Updated input and output costs for models in the catalog.\n\n## [16.2.7] - 2026-06-30\n\n### Fixed\n\n- Fixed compatibility with Kimi K2.7 Code on native endpoints to ensure thinking mode is preserved and tool choice is not forced.\n- Fixed Cerebras gemma-4-31b dynamic discovery to correctly identify the model as image-capable, enabling proper serialization of attached images.\n\n## [16.2.6] - 2026-06-29\n\n### Fixed\n\n- Fixed namespaced GLM-5.x model IDs on Z.AI/Zhipu OpenAI-compatible endpoints to inherit the widened stream watchdog, avoiding spurious stalled-stream errors during long thinking phases. ([#3819](https://github.com/can1357/oh-my-pi/issues/3819))\n\n## [16.2.3] - 2026-06-28\n\n### Added\n\n- Added support and configuration parameters for V2 streaming compaction in RemoteCompactionConfig, catalog types, and model/provider metadata.\n\n### Changed\n\n- Enabled automatic content markup healing for all OpenAI-compatible streaming models\n- Updated pricing and context window limits for several catalog models.\n- Disabled reasoning capability for multiple providers in the catalog.\n\n## [16.2.2] - 2026-06-27\n\n### Removed\n\n- Removed 'pi' from the list of supported dialects.\n\n## [16.2.0] - 2026-06-27\n\n### Added\n\n- Added GitLab Duo Agent catalog discovery, including namespace selection, live model mapping, and a bundled fallback model for fresh installs.\n- Added OpenAICompat.supportsNamedToolChoice to support forced tool use on string-only OpenAI-compatible chat servers without emitting the named function-object tool_choice shape.\n- Added model metadata support for provider-native remote compaction and compaction-only model selection.\n\n### Changed\n\n- Disabled the thinking-effort selector for GitLab Duo Agent models since the underlying platform parameters are server-fixed.\n\n### Fixed\n\n- Improved GitLab Duo Agent and Duo Workflow namespace and project discovery to robustly handle paginated groups, SSH remotes with custom ports, Git worktrees, self-managed GitLab instances with relative paths, and configuration via GITLAB_DUO_PROJECT_PATH or GITLAB_DUO_PROJECT_ID.\n- Fixed built-in LiteLLM discovery to prefer rich proxy metadata from management endpoints and avoid caching stale capability data.\n- Fixed GitLab Duo Workflow model specifications to resolve correct static context windows, enabling accurate context usage tracking and auto-compaction.\n\n## [16.1.23] - 2026-06-26\n\n### Added\n\n- Added `OpenAICompat.qwenPreserveThinking` — auto-enabled when the resolved `thinkingFormat` is `\"qwen\"` or `\"qwen-chat-template\"` AND `replayReasoningContent` is on (i.e. the four built-in local OpenAI-compatible providers, or a custom provider pointed at a loopback / RFC1918 / `*.local` baseUrl). Pairs with the chat-completions encoder change so the request body carries `preserve_thinking: true` (twin top-level + `chat_template_kwargs` emission), keeping Qwen3.6+ from stripping `<think>...</think>` off older assistant turns and breaking the local slot's KV cache between user messages. Non-Qwen chat templates ignore the parameter, so the flag stays a no-op outside the Qwen path; users on a cloud Qwen host (Alibaba Dashscope / Qwen Portal) can opt in with `compat.qwenPreserveThinking: true`. ([#3541](https://github.com/can1357/oh-my-pi/issues/3541))\n- Added CoreWeave Serverless Inference as an OpenAI-compatible provider with models.dev-backed bundled catalog metadata.\n\n## [16.1.22] - 2026-06-26\n\n### Added\n\n- Added `OpenAICompat.replayReasoningContent` — auto-enabled for the built-in local OpenAI-compatible providers (`llama.cpp`, `lm-studio`, `vllm`, `ollama` on `openai-completions`) and for any provider pointed at a loopback / RFC1918 / `*.local` baseUrl. NOT gated on `spec.reasoning`: the runtime discovery paths for `llama.cpp` / `lm-studio` / `openai-models-list` hardcode `reasoning: false` because the upstream `/models` endpoints don't advertise the capability, while the stream parser still records incoming `reasoning_content` deltas as thinking blocks — gating on the spec flag would leave every discovered local Qwen / DeepSeek model re-triggering #3528. The encoder only writes `reasoning_content` when a thinking block actually exists on the turn, so the flag is a no-op on pure-text histories. Built-in proxy providers (currently `litellm`) are excluded from both checks because they forward to an unrelated upstream that gains no KV-cache benefit and may 400 on the extra field; users running a custom proxy in front of a llama.cpp-style backend can opt in via the sparse `compat.replayReasoningContent: true` override. Signals to the `openai-completions` encoder that preserved `thinking` blocks must be re-emitted as `reasoning_content` on every assistant turn so chat templates that reconstruct `<think>…</think>` from the field (Qwen3, DeepSeek-R1, GLM-5.x) keep the prior turn's tokens byte-stable and llama.cpp's prefix KV cache survives. ([#3528](https://github.com/can1357/oh-my-pi/issues/3528))\n\n## [16.1.20] - 2026-06-25\n\n### Fixed\n\n- Fixed direct Anthropic Claude Sonnet/Haiku 4.5 advisor/agent turns crashing every call with HTTP 400 `This model does not support the effort parameter.` The catalog classified the whole Claude 4.5 family on `anthropic-messages` (and `bedrock-converse-stream`) as `anthropic-budget-effort`, which made the Anthropic provider serialize `output_config.effort` alongside `thinking.budget_tokens`. Anthropic only honors `output_config.effort` on Opus 4.5 and adaptive (4.6+) Messages-API models, so Sonnet 4.5 / Haiku 4.5 rejected the field. `inferThinkingControlMode` now gates `anthropic-budget-effort` to `parsedModel.kind === \"opus\" && semverGte(version, \"4.5\")` on both Anthropic-routed APIs, so Sonnet 4.5 / Haiku 4.5 on direct Anthropic + Cloudflare-AI-Gateway + Vertex + GitLab-Duo + Copilot + Bedrock fall through to plain `mode: \"budget\"` (thinking budget still scales with the selected effort tier). Opus 4.5 keeps `anthropic-budget-effort`. `anthropic-budget-effort` also stays in use for Anthropic-compatible third-party backends that natively support the field (Umans GLM 5.2). ([#3497](https://github.com/can1357/oh-my-pi/issues/3497))\n\n## [16.1.17] - 2026-06-24\n\n### Fixed\n\n- Fixed the Umans GLM-5.2 thinking-level picker collapsing to a single `high` tier after dynamic discovery: the `max` upstream level now resolves to the internal `xhigh` effort, the picker shows both `high` and `xhigh`, and the metadata maps `xhigh` back to Umans's native `max` wire tier. ([#3192](https://github.com/can1357/oh-my-pi/issues/3192))\n- Fixed GitHub Copilot business and enterprise endpoints accepting image inputs that they reject with `400 vision is not supported`. The Copilot `/models` response advertises `capabilities.supports.vision = true` for Claude/GPT chat models on every host, but only the canonical personal endpoint (`https://api.githubcopilot.com`) actually serves them; `githubCopilotModelManagerOptions` now forces `input: [\"text\"]` whenever discovery resolves to a non-personal base URL, and `mergeDynamicModel` honours the dynamic value (instead of OR-upgrading) when the merged endpoint differs from the bundled reference. ([#3387](https://github.com/can1357/oh-my-pi/issues/3387))\n- Fixed OpenRouter Anthropic compat to strip Responses reasoning history during replay so signed thinking blocks are not sent back to routed Anthropic providers. ([#3399](https://github.com/can1357/oh-my-pi/issues/3399))\n\n## [16.1.14] - 2026-06-22\n\n### Added\n\n- Added Sakana AI provider support with Fugu model integration\n- Added Sakana AI/Fugu provider catalog entries with Fugu model discovery and Responses API metadata\n- Added support for \"xhigh\" reasoning tier across model configurations\n- Added configuration for new models GCP-5.4 Mini, GPT-5.5, and variants\n- Added `devin` variant collapse table to streamline model tiering\n\n### Changed\n\n- Updated reasoning label pattern to include \"minimal\" and \"max\" efforts\n- Simplified model identification logic for Devin-powered reasoning models\n- Refactored variant routing to consolidate and standardize tier definitions\n\n## [16.1.13] - 2026-06-22\n\n### Added\n\n- Added support for Devin as a model provider\n- Added capability to fetch dynamic models from the Devin model manager\n\n## [16.1.11] - 2026-06-21\n\n### Fixed\n\n- Fixed Umans `umans-glm-5.1` / `umans-glm-5.2` advertising native image input. The `models/info` endpoint reports `supports_vision: \"via-handoff\"` for the GLM models, meaning vision routes through a separate handoff pre-analysis step instead of accepting raw image blocks; `umansSupportsVision` treated any non-empty string as native vision support, so image prompts went directly to GLM and were rejected with `400 This model does not support image inputs`. The helper now requires `supports_vision === true`, the bundled GLM 5.1/5.2 rows are corrected to text-only, and stale mismatched Umans cache rows for those ids are dropped so the vision-handoff path runs even before a successful refresh. ([#3184](https://github.com/can1357/oh-my-pi/issues/3184))\n\n## [16.1.9] - 2026-06-21\n\n### Fixed\n\n- Fixed the `moonshot` provider with no path to the Kimi China API: model discovery now honors a `MOONSHOT_BASE_URL` override (redirecting to `api.moonshot.cn`), and `KIMI_API_KEY` resolves as a fallback for `MOONSHOT_API_KEY`. ([#2883](https://github.com/can1357/oh-my-pi/issues/2883))\n- Fixed LiteLLM model discovery preserving colliding models.dev transport metadata (for example `ollama-cloud` `deepseek-v4-flash`) instead of keeping the LiteLLM `openai-completions` provider transport. ([#3162](https://github.com/can1357/oh-my-pi/issues/3162))\n\n### Removed\n\n- Removed bundled Wafer Pass (`wafer-pass`) catalog entries and generation support; Wafer Serverless remains available as `wafer-serverless`.\n\n## [16.1.8] - 2026-06-20\n\n### Fixed\n\n- Fixed Fireworks-hosted Qwen turns (e.g. `fireworks/qwen3.7-plus`) failing with `400 Extra inputs are not permitted, field: 'enable_thinking'`. Fireworks serves Qwen3 with controllable thinking via OpenAI-style `reasoning_effort` and rejects the top-level `enable_thinking` boolean that Alibaba DashScope speaks; `buildOpenAICompat` was selecting `thinkingFormat: \"qwen\"` from the `qwen` id pattern regardless of host. Fireworks-hosted Qwen models now resolve to `thinkingFormat: \"openai\"`.\n- Fixed MiMo models on OpenAI-compatible gateways to expose only accepted `low`, `medium`, and `high` reasoning tiers and map unsupported raw `minimal`/`xhigh` requests to safe wire values. ([#2864](https://github.com/can1357/oh-my-pi/issues/2864))\n\n## [16.1.7] - 2026-06-20\n\n### Fixed\n\n- Fixed MiniMax-M3 catalog context for the MiniMax Coding/Token Plan providers `minimax-code` and `minimax-code-cn` to report the documented 1M long-context tier instead of the upstream 512K pricing boundary; the previous patch only covered `minimax`/`minimax-cn`, so the Coding Plan picker still showed 512K in the status bar ([#3097](https://github.com/can1357/oh-my-pi/issues/3097)).\n\n## [16.1.4] - 2026-06-19\n\n### Fixed\n\n- Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families so every effort and off resolve to the live wire id. Added `claude-sonnet-4-6` and `claude-opus-4-6-thinking` entries to `ANTIGRAVITY_MODEL_WIRE_PROFILES` capped at the backend's 64000-output-token limit (over-cap requests 400'd with `Request contains an invalid argument`); `modelEnum` is now optional on `AntigravityModelWireProfile` since the Claude wire ids are accepted without a captured `labels.model_enum`. ([#3067](https://github.com/can1357/oh-my-pi/issues/3067))\n\n## [16.1.3] - 2026-06-19\n\n### Fixed\n\n- Marked Ollama Cloud catalog models to omit on-the-wire output-token caps, preventing context-window-sized `num_predict` values from causing HTTP 400s for models whose true output cap is not discoverable. ([#2984](https://github.com/can1357/oh-my-pi/issues/2984))\n- Fixed `readModelCache`/`writeModelCache` using a process-global shared database even when a custom `dbPath` was provided. Custom-path cache operations now open and close a per-call database via `withModelCacheDb`, preventing leaked SQLite handles on Windows\n\n## [16.1.2] - 2026-06-19\n\n### Added\n\n- Added support for Gemini 2.5 Flash-Lite, 3.1 Flash-Lite, and 3.5 Flash models\n- Added support for Moonshot V1 model family\n\n### Changed\n\n- Updated context window and token limits for various Claude, Gemini, and GPT-OSS models\n- Refined thinking mode behaviors and routing for supported LLM families\n\n### Fixed\n\n- Fixed GLM-5.2 `reasoning_effort` so the top thinking tier reaches each host's genuine maximum instead of 400ing, mapping the internal `xhigh` tier per host dialect (verified against live endpoints): Z.ai/Zhipu collapse onto the model's `none`/`high`/`max` scale (`xhigh → max`); Fireworks, resellers, and Ollama Cloud keep their distinct lower tiers and remap only the top `xhigh → max` (merged over host quirks such as Fireworks' `minimal → none`); and OpenRouter — whose API rejects `max` and treats `xhigh` as its own max tier — now exposes the `xhigh` tier and forwards it verbatim. Dialect detection keys off resolved `compat.thinkingFormat`, so custom OpenRouter/Z.ai-format providers are covered too.\n- Maintained thinking effort routing when discovery only returns the base model ID\n- Improved credential retrieval logic for Antigravity and Codex providers via auth discovery\n\n## [16.0.9] - 2026-06-18\n\n### Fixed\n\n- Fixed GitHub Copilot's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`). It forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: \"\"` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused a `400 Invalid signature` that corrupted the session and re-tripped on every full history re-send (e.g. after toggling MCP servers). Copilot now degrades such blocks to text like the official API. ([#2851](https://github.com/can1357/oh-my-pi/issues/2851))\n- Added a `supportsImageDetailOriginal` compat flag that resolves to `false` for GitHub Copilot, whose Responses endpoint rejects the `detail: \"original\"` image hint with a 400, and `true` for every other host. ([#2822](https://github.com/can1357/oh-my-pi/issues/2822))\n\n## [16.0.8] - 2026-06-18\n\n### Changed\n\n- Refactored model family ID predicates and capability checkers to use a shared, uniform process-lifetime `memo` utility to eliminate caching boilerplate.\n\n### Fixed\n\n- Fixed LM Studio dynamic discovery to use native `/api/v0/models` metadata so VLM models advertise image input. ([#2945](https://github.com/can1357/oh-my-pi/issues/2945))\n\n## [16.0.7] - 2026-06-18\n\n### Fixed\n\n- Fixed MiniMax Anthropic-compatible M2/M3 thinking metadata to expose the adaptive transport and keep M2 mandatory reasoning floored ([#2928](https://github.com/can1357/oh-my-pi/issues/2928)).\n\n## [16.0.6] - 2026-06-18\n\n### Added\n\n- Added a dedicated `openrouter` API type and `ResolvedOpenRouterCompat` configuration to support unified chat-completions and Responses-API compatibility for OpenRouter models\n\n### Changed\n\n- Migrated bundled OpenRouter models in the catalog from `openai-completions` to the new `openrouter` API type\n- Consolidated the resolved OpenAI compat shape: extracted a shared `ResolvedOpenAISharedCompat` core that both `ResolvedOpenAICompat` and `ResolvedOpenAIResponsesCompat` extend (each builder still computes its own per-surface value, preserving chat↔Responses divergence), added internal resolved wire-quirk fields (`wireModelIdMode`, `stripDeepseekSpecialTokens`, `reasoningDeltasMayBeCumulative`, `emptyLengthFinishIsContextError`, `usesOpenAIToolCallIdLimit`, `dropThinkingWhenReasoningEffort`, `supportsObfuscationOptOut`), and replaced `buildOpenRouterCompat`'s cast-and-copy with an exhaustive `pickResponsesOnly` composition that fails to compile if a new Responses-only field is added without handling. The public `OpenAICompat` config vocabulary is unchanged.\n- Expanded `OpenAICompat`/`ResolvedOpenAISharedCompat` with shared reasoning/history/stream/request flags (`reasoningDisableMode`, `omitReasoningEffort`, `includeEncryptedReasoning`, `filterReasoningHistory`, `requiresReasoningContentForAllAssistantTurns`, `streamMarkupHealingPattern`, `promptCacheSessionHeader`, etc.) so model/provider/gateway constraints are declared once in catalog compat and then consumed uniformly by Chat Completions and Responses endpoints.\n\n### Fixed\n\n- Changed the default compatibility builder for `openai-completions` to set `requiresAssistantAfterToolResult` to `isMistral`, enabling the synthetic assistant bridge for built-in Mistral and Devstral models.\n- Fixed local Ollama (`provider: \"ollama\"`) reasoning turns still failing with HTTP 400 `invalid reasoning value: \"minimal\"` when the model was selected from a stale `~/.omp/models.db` cache row or a hand-written config: the `minimal → low` / `xhigh → max` remap was only stamped during fresh discovery, so cached and custom specs reached the wire unmapped. The remap now lives in the OpenAI chat-completions and Responses compat builders, so every `buildModel` (including cache loads, custom specs, and the `whenThinking` variant) backfills it — no `omp models refresh` required. Custom OpenAI-compatible providers registered under a non-`ollama` provider id still need their own `compat.reasoningEffortMap`.\n- Advertised Ollama Cloud GLM-5.2 reasoning efforts as high/xhigh-only and mapped `xhigh` to native max effort ([#2911](https://github.com/can1357/oh-my-pi/pull/2911) by [@serverinspector](https://github.com/serverinspector))\n- Fixed OpenRouter pseudo-API model construction so bundled OpenRouter models resolve shared OpenAI compatibility metadata instead of an undefined compat record.\n- Fixed custom/direct `xai-oauth` Responses model specs (e.g. `grok-build`) emitting `reasoning.effort` and hitting xAI's HTTP 400: `buildOpenAIResponsesCompat` now defaults `supportsReasoningEffort` to `false` for `xai-oauth` Grok models that are off the effort-capable allowlist (`grok-3-mini`/`grok-4.20-multi-agent`/`grok-4.3`), matching the curated discovery path; explicit `compat.supportsReasoningEffort` still overrides. The allowlist moved to a shared `isGrokReasoningEffortCapable` identity helper consumed by both the compat builder and provider-model curation so the two cannot drift.\n\n## [16.0.5] - 2026-06-17\n\n### Added\n\n- Added `enableGeminiThinkingLoopGuard` to OpenAI compatibility options to allow explicit opt-in or opt-out of the Gemini thinking-loop guard for OpenAI-compatible model aliases\n- Added `LITELLM_BASE_URL` as the LiteLLM provider discovery base URL fallback, with discovery caches scoped by the resolved proxy URL and explicit provider `baseUrl` config kept at higher precedence. ([#2726](https://github.com/can1357/oh-my-pi/issues/2726))\n- Added `ThinkingConfig.effortBudgets` (per-effort thinking-budget contract baked into collapsed variants) and `ANTIGRAVITY_MODEL_WIRE_PROFILES` (`maxOutputTokens` + `model_enum` per Antigravity wire id) to mirror the captured Antigravity Cloud Code Assist client request shape.\n\n### Changed\n\n- Defaulted `enableGeminiThinkingLoopGuard` from Gemini family detection for both OpenAI completions and responses compatibility specs so Gemini models now enable the thinking-loop guard automatically\n- Updated the default Gemini CLI user-agent version fallback to 0.46.0.\n- Changed the Antigravity (`google-antigravity`, daily-cloudcode-pa) gemini-3.x collapse families to the `budget` thinking transport with the client's per-tier `thinkingBudget` (3.5 Flash low/medium/high = 1000/4000/10000, 3.1 Pro low/high = 1001/10001) and corrected 3.5 Flash effort→wire routing (medium → `gemini-3.5-flash-low`, high → `gemini-3-flash-agent`). Split the shared CCA collapse table so `google-gemini-cli` (cloudcode-pa) keeps the `google-level` `thinkingLevel` transport for official Gemini CLI parity. Stale collapsed snapshots (bundled catalog, recycled `gemini-3-flash` alias) self-heal from the hand table at collapse time, and the model cache schema is bumped to v7 to invalidate pre-budget Antigravity rows.\n- Changed the Antigravity user-agent to the `antigravity/hub/<version>` format (default `2.1.4`) to match the captured client.\n\n### Fixed\n\n- Fixed `off` effort routing for `claude-opus-4-5` and `claude-opus-4-6` to use their base model IDs when thinking is disabled\n- Fixed `gemini-2.5-flash` effort routing so all non-off effort levels resolve to `gemini-2.5-flash-thinking`\n- Fixed shared variant alias provider resolution so `resolveBareVariantAlias` reports all matching providers when model aliases are present in both CCA collapse tables\n- Routed google-antigravity default baseUrl to the stable primary daily endpoint in the catalog generator and all fallback snapshots, resolving connection drops on heavy queries.\n- Fixed MiniMax M3 dialect selection so MiniMax-family OpenAI-compatible models use the MiniMax tool-call dialect instead of generic XML. ([#2759](https://github.com/can1357/oh-my-pi/issues/2759))\n- Fixed GitHub Copilot dynamic discovery to honor plan-specific API endpoints stored in structured OAuth credentials. ([#2876](https://github.com/can1357/oh-my-pi/issues/2876))\n\n## [16.0.4] - 2026-06-17\n\n### Fixed\n\n- Fixed GLM-5.2 catalog thinking metadata for Zhipu/BigModel so the top effort is exposed as `xhigh` and maps to provider-native `max`. ([#2833](https://github.com/can1357/oh-my-pi/issues/2833))\n\n## [16.0.2] - 2026-06-16\n\n### Fixed\n\n- Fixed Kimi output caps for Umans AI Coding Plan and Venice so discovery metadata cannot use context-sized token ceilings as request caps.\n- Marked Umans Anthropic-compatible models as client-tool escaped so cached and bundled metadata do not expose `web_search` as a provider server tool.\n\n## [16.0.1] - 2026-06-15\n\n### Added\n\n- Added the Umans AI Coding Plan provider catalog with Anthropic-compatible model metadata and dynamic discovery ([#2636](https://github.com/can1357/oh-my-pi/pull/2636) by [@oldschoola](https://github.com/oldschoola)).\n\n## [16.0.0] - 2026-06-15\n\n### Breaking Changes\n\n- Renamed the catalog-owned tool syntax API from `ToolCallSyntax`/`FALLBACK_TOOL_SYNTAX`/`preferredToolSyntax` to `Dialect`/`FALLBACK_DIALECT`/`preferredDialect`.\n\n## [15.13.3] - 2026-06-15\n\n### Added\n\n- Added Azure OpenAI as a catalog provider (`azure`, default model `gpt-5.5`, env var `AZURE_OPENAI_API_KEY`), bundling the OpenAI-family models Azure serves over the Responses API (GPT-4/4.1/4o, GPT-5 family, o-series, Codex). Like Amazon Bedrock it is catalog-only — models ship in the bundle and become selectable once the env key is set, with the deployment base URL resolved at runtime from `AZURE_OPENAI_BASE_URL`/`AZURE_OPENAI_RESOURCE_NAME`.\n- Added models.dev-backed bundled catalogs for providers that previously shipped no offline models: Hugging Face, Kilo, Moonshot, NanoGPT, Synthetic, Venice, Ollama Cloud, and the Xiaomi Token Plan regions (ams/cn/sgp). They still discover live when credentialed; the bundle is now a non-empty baseline.\n\n### Changed\n\n- Updated stale provider default models to their latest bundled versions: OpenAI-family providers (`azure`, `github-copilot`, `aimlapi`) → GPT-5.5; Gemini providers (`google`, `google-gemini-cli`, `google-vertex`) → `gemini-3.1-pro-preview`; GLM providers (`zai`, `zhipu-coding-plan`) → `glm-5.2`, `cerebras` → `zai-glm-4.7`; Kimi providers (`fireworks`, `opencode-go`, `moonshot`) → `kimi-k2.7-code`, `kimi-code` → `kimi-for-coding`, `together` → `moonshotai/Kimi-K2.7-Code`; `alibaba-coding-plan` → `qwen3.7-plus`; and Claude-Sonnet defaults (`cloudflare-ai-gateway`, `cursor`, `gitlab-duo`, `kilo`, `opencode-zen`, `vercel-ai-gateway`) → Claude Opus 4.x.\n- Restricted models.dev Azure discovery to OpenAI-family IDs (`gpt-`, `o1`, `o3`, `o4`, `codex`, `chatgpt`), excluding Foundry-hosted third parties (Claude/DeepSeek/Llama/Mistral/Phi) that Azure serves through non-Responses APIs.\n- Detected the Azure OpenAI Responses compat surface (developer role, strict tool mode, strict tool-result pairing) by provider id as well as base URL, so bundled `azure` models whose deployment host is only known at runtime still get the right wire behavior.\n- Renamed the `Qwen3-ASR-Flash` model label to `Qwen3 ASR Flash`\n\n### Fixed\n\n- Fixed tool syntax selection for Gemini-family and Gemma model IDs by routing them to dedicated `gemini` and `gemma` formats instead of generic XML\n- Fixed `zhipu-coding-plan` and `together` shipping no bundled models: their descriptors referenced non-existent models.dev keys (`zhipu-coding-plan`, `together`); pointed them at the real keys (`zhipuai-coding-plan`, `togetherai`) so they bundle their GLM and full catalogs respectively.\n- Folded the `azure-openai-responses` API into the OpenAI Responses thinking-inference branches so Azure reasoning models (o-series, GPT-5, Codex) resolve the discrete effort vocabulary (including `xhigh`) and effort-control mode instead of falling through to generic defaults.\n- Fixed `ollama-cloud` discovery inheriting an unsafe cross-provider `contextWindow`/`maxTokens` when `/api/show` returns no size metadata; it now falls back to the safe 128K context / 8K output caps.\n- Dropped internal Fireworks control-plane resource ids (`accounts/fireworks/{models,routers}/…`) from the bundle; only the public request ids ship.\n\n## [15.13.2] - 2026-06-15\n\n### Added\n\n- Added the `ToolCallSyntax` union and `FALLBACK_TOOL_SYNTAX` constant to `@oh-my-pi/pi-catalog/identity` (re-exported from `@oh-my-pi/pi-ai/grammar`).\n- Added `preferredToolSyntax(modelId)` to `@oh-my-pi/pi-catalog/identity`, resolving a model's native tool-call syntax affinity from its family token (Claude→`anthropic`, GLM→`glm`, Kimi→`kimi`, Qwen→`qwen3`, DeepSeek→`deepseek`, OpenAI/gpt-oss→`harmony`, else the `xml` fallback).\n- Added `flux-1-schnell-fp8` to the Fireworks serverless model catalog\n- Added `gpt-oss-20b` to the Fireworks model catalog\n- Added `qwen3-embedding-8b` to the Fireworks model catalog\n- Added `qwen3-reranker-8b` to the Fireworks model catalog\n- Added `Gemma 4 E2B IT` and `Gemma 4 E4B IT` to the Google model catalog\n- Added `qwen/qwen3-asr-flash` to the Zenmux model catalog\n- Added sparse `supportsTools` model metadata so providers can mark models that require in-band tool-call formatting.\n\n### Changed\n\n- Kept non-tool-capable Fireworks serverless models in discovery results and marked them with `supportsTools: false` for fallback-aware handling\n- Extended `modelFamilyToken(modelId)` to classify Claude/OpenAI ids the structured parser misses (older dated forms such as `claude-3-5-sonnet-20241022` and `gpt-4o`), returning `anthropic`/`openai` instead of an empty token.\n\n## [15.13.1] - 2026-06-15\n\n### Added\n\n- Added `modelFamilyToken(modelId)` to `@oh-my-pi/pi-catalog/identity`: a coarse vendor-lineage token (`anthropic`/`openai`/`gemini`/`kimi`/…) for \"are two models the same family?\" comparisons, backed by `parseKnownModel` canonical-id normalization. Opaque and comparison-only; kind/variant collapsed onto the vendor token ([#2406](https://github.com/can1357/oh-my-pi/issues/2406))\n\n### Changed\n\n- Changed catalog metadata to update a model’s per-token pricing to input 0.09 and output 0.18\n- Changed the same cataloged model’s maximum token limit from 384000 to 65536\n\n### Fixed\n\n- Fixed MiniMax-M3 catalog context for `minimax` and `minimax-cn` to report the documented 1M long-context tier instead of the upstream 512K pricing boundary ([#2576](https://github.com/can1357/oh-my-pi/issues/2576)).\n- Fixed OpenCode Go MiMo catalog metadata so title generation and other tool-enabled calls omit unsupported `tool_choice` instead of triggering provider 400s ([#2509](https://github.com/can1357/oh-my-pi/issues/2509)).\n- Fixed OpenCode Go `kimi-k2.7-code` catalog metadata so resolve-gate requests use automatic tool selection instead of Moonshot-rejected forced `tool_choice` ([#2546](https://github.com/can1357/oh-my-pi/issues/2546)).\n- Fixed Anthropic compat for the `github-copilot` host so `supportsEagerToolInputStreaming` defaults to `false` there, matching the Copilot proxy which rejects the per-tool `eager_input_streaming` field ([#2558](https://github.com/can1357/oh-my-pi/issues/2558)).\n- Scoped vLLM model cache validity to the discovery base URL so changed endpoints refetch immediately, and bounded built-in vLLM discovery requests with a timeout.\n\n## [15.12.6] - 2026-06-14\n\n### Added\n\n- Added GLM-5.2 to the bundled zai (GLM Coding Plan) catalog as the selectable 1M served model.\n\n### Changed\n\n- Pinned zai `glm-5.2` to 1M context during catalog generation so endpoint discovery and older fallbacks cannot regress it to 200k.\n- Replaced the hand-maintained `zhipu-coding-plan` GLM reasoning allowlist and vision regex with a `parseGlmModel` family classifier in `identity/classify.ts` (variant + vision + version), surfaced as `isReasoningGlmModelId` / `isGlmVisionModelId`. Discovery now derives reasoning/vision capability from the GLM family instead of a per-id list, so newly-bumped integers (`glm-5.3`, `glm-6`, …) are covered automatically while `-flash`/`-preview` and the vision `…v` shape stay correctly classified.\n\n## [15.12.4] - 2026-06-13\n\n### Added\n\n- Added bundled Fireworks models `deepseek-v4-flash`, `kimi-k2.7-code`, `minimax-m2.5`, `minimax-m3`, `nemotron-3-ultra-nvfp4`, `qwen3.6-plus`, and `qwen3.7-plus`\n- Changed\n\n### Changed\n\n- Model `contextWindow`/`maxTokens` are now `number | null`; discovery emits `null` when a provider reports no limit, replacing the `222222`/`8888` (`UNK_CONTEXT_WINDOW`/`UNK_MAX_TOKENS`) sentinels (now removed). Bundled `models.json` unknown limits are `null`.\n- Changed the `github-copilot` model context window to `524288` tokens\n- Changed Fireworks model discovery to source the control-plane `List Models` API (`GET /v1/accounts/fireworks/models?filter=supports_serverless=true`) instead of the OpenAI-compatible `/v1/models` inference listing. The inference endpoint returns a sparse, account-specific subset that omits on-demand serverless models (e.g. `kimi-k2.7-code`), so newly published serverless models stayed invisible in the picker until hand-added to the bundled catalog. The control-plane catalog enumerates every serverless model with capability metadata (`supportsServerless`/`supportsTools`/`supportsImageInput`/`contextLength`/`displayName`), paginated and filtered to tool-capable `READY` entries, then merged with bundled/models.dev references — the Kimi K2 max-output clamp and DeepSeek V4 thinking-toggle strip are preserved, and unbundled models default to reasoning so `buildModel` derives the Fireworks effort map. New serverless releases now surface automatically with no catalog edits.\n\n### Fixed\n\n- Filled missing `contextWindow` and `maxTokens` in generated `models.json` for proxy/reseller variants by inheriting limits from canonical-family and segment-reference models\n- Ignored zero-cost `x-ai` subscription entries as reference sources when backfilling limits so inflated values are not propagated\n- Fixed the model cache opening with `PRAGMA journal_mode=WAL` before `PRAGMA busy_timeout`, so concurrent omp startups could crash inside `getDb()` on `SQLITE_BUSY` during WAL recovery instead of waiting through the transient lock. The busy handler is now installed before the first lock-taking statement ([#2421](https://github.com/can1357/oh-my-pi/issues/2421)).\n\n## [15.11.8] - 2026-06-12\n\n### Fixed\n\n- Fixed Antigravity `gemini-3.1-pro --thinking high` failing with `Cloud Code Assist API error (400): Request contains an invalid argument.` — the upstream `gemini-3.1-pro-high` deployment rejects every `streamGenerateContent` request on both CCA endpoints while discovery still advertises it. High effort now routes to `gemini-pro-agent` (the same \"Gemini 3.1 Pro (High)\" model, verified accepting the identical request body), and the model-cache fingerprint version was bumped (`merge-v2` → `merge-v3`) so existing fresh caches refetch discovery and pick up the corrected routing immediately.\n\n## [15.11.7] - 2026-06-12\n\n### Added\n\n- Added effort-tier variant collapsing (`variant-collapse`): providers that expose one logical model as several effort/thinking-suffixed upstream ids (Antigravity CCA `gemini-3.5-flash-extra-low`/`-low`/`gemini-3-flash-agent`, `gemini-3[.1]-pro-low|high`, `claude-*[-thinking]` pairs, `gpt-oss-120b-medium`) collapse into one logical entry carrying per-effort upstream routing in `thinking.effortRouting` (plus `thinking.suppressWhenOff` for Cloud Code Assist ids whose baked server default re-applies when `thinkingConfig` is omitted). Request-time code resolves the outbound id via `resolveWireModelId(model, effort)`; selection, caching, and usage attribution key on the logical id.\n- Added the automatic `X`/`X-thinking` pair rule (`deriveThinkingPairFamilies`): any provider's live bare/thinking twin collapses into the bare id, routing thinking-enabled requests to the `-thinking` backing id (trailing or infix token, so `kimi-k2-thinking-turbo` pairs with `kimi-k2-turbo`). Gated on same api and compatible pricing — all-zero cost rows count as unknown, while twins that both carry real, differing prices remain separate SKUs.\n- Added `collapseBuiltModelVariants` and wired collapsing at every materialization point — Antigravity discovery, the catalog generator, and the model-manager merge — so stale sources (old static beside collapsed dynamic results, mixed cache rows) converge on logical entries instead of unioning raw tier ids back into the catalog.\n- Added `thinking.requiresEffort`, baked for reasoning-only upstreams — Gemini 3.x (levels only, no off), Gemini 2.5 Pro (thinkingBudget floors at 128, rejects 0), OpenAI o-series, MiniMax M2, and thinking-variant SKUs (`*-thinking`/`*-reasoner`/`*-reasoning`, with a negation-aware token grammar so `non-thinking` ids never match). Identity derivation bakes it for new entries and `fillThinkingWireDefaults` backfills explicit/cached metadata; `minimumSupportedEffort` exposes the canonical floor. Pair-collapsed twins drop member flags (their off routes to the bare SKU), while identity re-flags pairs whose logical id is itself mandatory\n\n### Changed\n\n- Changed model display names to drop model-extrinsic decorations: gateway author prefixes (`OpenAI: …`, `Google: …`), `(latest)` alias markers, `(Antigravity)` provider attribution, price tiers (`($$$$)`), and promo/lifecycle tags (`(20% off)`, `(retires …)`). `cleanModelName` is applied in `buildModel` (covers live discovery and stale caches) and as a catalog-generator pass; Antigravity discovery no longer appends `(Antigravity)` to display names. Variant tags that map to distinct wire ids (`(Thinking)`, `(free)`, `(Fast)`, dates, regions) are preserved.\n- Changed the `google-antigravity` default model from `gemini-3-pro-high` to `gemini-3.1-pro`\n- Changed `gemini-2.5-flash-thinking` handling from discovery-denylist to collapsing into `gemini-2.5-flash` (thinking-enabled requests route to the `-thinking` backing id)\n- Bumped the model cache schema to v5 so rows predating effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids) are invalidated\n\n### Fixed\n\n- Fixed catalog generation to apply effort-tier variant collapsing before provider grouping to ensure collapsed model families are consistently materialized without being impacted by in-loop mutation\n- Fixed Kimi K2.6 OpenAI-compatible compat metadata to use a 300s stream watchdog floor, covering Fire Pass router ids as well as public `kimi-k2.6` ids so long reasoning starts do not hit the generic first-event timeout ([#2366](https://github.com/can1357/oh-my-pi/issues/2366)).\n\n## [15.11.4] - 2026-06-12\n\n### Fixed\n\n- Fixed MiniMax M2-family and OpenAI gpt-oss model metadata so OpenAI-compatible catalog entries declare only `low|medium|high` thinking efforts. Their upstreams reject `minimal`, `xhigh`, and Fireworks' `minimal → none` wire mapping, so `fireworks/minimax-m2.7` as the smol auto-thinking classifier model 400ed on every turn. OpenAI-compatible provider effort maps (`Groq qwen/qwen3-32b`, DeepSeek-family, OpenRouter Anthropic adaptive, Fireworks `minimal → none`) now bake into `thinking.effortMap` in catalog metadata instead of `buildOpenAICompat`, and request builders read that field directly. Regenerated `models.json` now makes `disableReasoning` choose `low` for those families while leaving GLM-5.x and other Fireworks models on the existing `minimal → none` path ([#2315](https://github.com/can1357/oh-my-pi/issues/2315)).\n\n### Added\n\n- Added `requiresJuiceZeroHack` Responses-API compat flag, resolved by `buildOpenAIResponsesCompat` from GPT-5-family model names and overridable via sparse model `compat` config. Replaces the request-time `model.name.startsWith(\"gpt-5\")` sniff that gated the trailing `# Juice: 0 !important` no-reasoning developer item.\n\n## [15.11.3] - 2026-06-11\n\n### Added\n\n- Added `requestModelId` on `Model` to represent the upstream model id used when a catalog entry is a local variant\n- Added synthetic GitHub Copilot long-context model variants with `-1m` suffixes when tiered token pricing is advertised\n\n### Changed\n\n- Changed GitHub Copilot discovery to request `X-GitHub-Api-Version: 2026-06-01` from `api.githubcopilot.com`\n- Changed GitHub Copilot discovery to cap base model `contextWindow` to the default token tier and keep long-context access as the separate `-1m` model entry\n- Changed Copilot model mapping to omit non-chat `/models` entries and enable image input for models whose capabilities indicate vision support\n\n### Fixed\n\n- Fixed long-context variant pricing to use `billing.token_prices.long_context` rates instead of default model pricing\n- Fixed `mapModel` handling in OpenAI-compatible discovery so returning `null` now skips a model entry rather than falling back to defaults\n- Fixed model ID precedence so a real upstream Copilot model id is kept when it conflicts with a synthesized `-1m` variant\n\n## [15.11.1] - 2026-06-11\n\n### Fixed\n\n- Fixed NVIDIA NIM Qwen turns failing with `400 Validation: Unsupported parameter(s): enable_thinking`. NIM's chat-completions schema is `additionalProperties: false` and exposes thinking via the vLLM convention `chat_template_kwargs.enable_thinking`; `buildOpenAICompat` was sending top-level `enable_thinking` for every `qwen/*` id regardless of host. Registered `nvidia` as a known host (`integrate.api.nvidia.com`) and routed NVIDIA-hosted Qwen models to `thinkingFormat: \"qwen-chat-template\"` ([#2299](https://github.com/can1357/oh-my-pi/issues/2299)).\n- Fixed Moonshot/Kimi native OpenAI-compatible request metadata so Kimi K2 uses `max_tokens` and omits OpenAI-only `store`, restoring first-turn output with `MOONSHOT_API_KEY` ([#2289](https://github.com/can1357/oh-my-pi/issues/2289)).\n\n## [15.11.0] - 2026-06-10\n\n### Fixed\n\n- Fixed `buildModel` so malformed explicit thinking metadata without `efforts` is treated as sparse input and inferred instead of crashing during model resolution ([#2251](https://github.com/can1357/oh-my-pi/issues/2251)).\n\n## [15.10.12] - 2026-06-10\n\n### Added\n\n- Added `grok-composer-2.5-fast` (Cursor \"Composer 2.5 Fast\") to the xAI Grok OAuth (SuperGrok) catalog: non-reasoning, text-only, 200K context.\n\n### Changed\n\n- Set every xAI Grok OAuth (SuperGrok) curated model's max output tokens to mirror its context window (`grok-build`, `grok-4.3`, `grok-4.20-0309-{reasoning,non-reasoning}`, `grok-4.20-multi-agent-0309`, `grok-composer-2.5-fast`), replacing the `8888` `UNK_MAX_TOKENS` placeholder (and a stale `30000` on three grok-4.x entries). xAI's OAuth `/v1/models` reports no per-request output limit, so the curated catalog now owns `maxTokens` like `contextWindow`, deterministic on both the static-seed and online-overlay paths; the `openai-responses` wire still clamps the actual request to `OPENAI_MAX_OUTPUT_TOKENS` (64k).\n\n### Fixed\n\n- Excluded zero-cost `xai-oauth` subscription entries from the model reference indexes (`buildModelReferenceIndex`, `createReferenceResolver`), so their zero pricing and context-window-sized `maxTokens` cannot outrank paid/public Grok references when resolving custom-provider model identities.\n\n## [15.10.11] - 2026-06-10\n\n### Added\n\n- Added `hostMatchesUrl`, `modelMatchesHost`, and endpoint-shape helpers in the new `hosts` module for consistent provider/baseUrl matching\n- `buildModel(spec)` (`build.ts`) is now the single Model constructor: it materializes the fully-resolved compat record and canonical thinking metadata exactly once (compat first, thinking derived from identity + resolved compat), so `Model.compat` is a required, complete `CompatOf<TApi>` (`ResolvedOpenAICompat`/`ResolvedOpenAIResponsesCompat`/`ResolvedAnthropicCompat`) and request-path code reads fields with zero URL parsing and zero per-request allocation. Sparse user/config overrides live on the new `ModelSpec<TApi>` input shape and survive on `Model.compatConfig` for introspection.\n- Added `ResolvedAnthropicCompat.supportsSamplingParams` (Opus 4.7+/Fable/Mythos reject `temperature`/`top_p`/`top_k` with a 400), baked at build time from model identity so the request path stops re-parsing model ids.\n- Compat detection gained model-time flags so handlers stop sniffing baseUrl: completions `supportsReasoningParams`, `alwaysSendMaxTokens`, `isOpenRouterHost`, `isVercelGatewayHost`, `streamIdleTimeoutMs`, and a precomputed `whenThinking` alternate view (OpenCode `reasoning_content` gating, #1071/#1484); responses `strictResponsesPairing`, `supportsLongPromptCacheRetention`, `supportsReasoningEffort`; anthropic `officialEndpoint`, `requiresToolResultId`, `replayUnsignedThinking`.\n- New `@oh-my-pi/pi-catalog` package: the model catalog extracted from `@oh-my-pi/pi-ai`. Owns the bundled `models.json` and its generation pipeline (`scripts/generate-models.ts`), the core model data types (`Model`, `Api`, `ThinkingConfig`, `Effort`, `Usage`, compat interfaces), thinking metadata enrichment and generated policies (`model-thinking.ts`), the SQLite model cache and model manager, per-provider discovery factories (`provider-models/`), the discovery protocol clients (`discovery/`), and the new `CATALOG_PROVIDERS` table — the single source of truth for provider ids, default models, and discovery wiring (`KnownProvider`, `PROVIDER_DESCRIPTORS`, and `DEFAULT_MODEL_PER_PROVIDER` are derived from it).\n- New `identity/` module centralizing model-identity concerns that were previously duplicated across packages: family classification and version parsing (`identity/classify.ts`, extracted from pi-ai's `model-thinking` internals), canonical model equivalence with injected reference data (`identity/equivalence.ts`, from coding-agent's `model-equivalence`), proxy/reseller reference lookup (`identity/reference.ts`, from coding-agent's `model-registry`), bracket-affix and id-segment helpers (`identity/id.ts`), a single trailing-marker vocabulary with canonical vs reference flavors (`identity/markers.ts` — `search` stays reference-only so Perplexity's `sonar-pro-search` remains canonical-distinct), and provider priority ordering (`identity/priority.ts`).\n- Memoized bundled-reference accessors (`getBundledCanonicalReferenceData` / `getBundledModelReferenceIndex` in `identity/bundled.ts`): one lazy walk of the bundled catalog feeds both canonical equivalence and proxy-reference lookup, so consumers no longer hand-roll the glue.\n- `identity/selection.ts`: pure canonical-variant selection (`resolveCanonicalVariant`, `buildCanonicalModelOrder`, `CanonicalVariantPreferences`) extracted from the coding-agent registry — provider rank, then exact-id match, variant source, id length, and candidate order.\n\n### Changed\n\n- Changed OpenAI compatibility detection to use shared host classifiers (`modelMatchesHost`/`hostMatchesUrl`) with normalized matching instead of raw URL substring checks\n- Changed `hostMatchesUrl`/`modelMatchesHost` usage in compatibility detection to reduce mismatches across case variants and provider alias hosts\n- Provider catalog entries now carry the runtime API-key env fallback as an ordered `envVars` list; `catalogDiscovery.envVars` became an optional generation-time override (only `cursor` and `vercel-ai-gateway` differ) and `PROVIDER_DESCRIPTORS` materializes the resolved list for `generate-models.ts`.\n- `Model`'s api parameter now defaults to `Api` instead of `any` (`Model<TApi extends Api = Api>`), so bare `Model` no longer behaves as `Model<any>` at call sites.\n- `ThinkingConfig` is now explicit and total: an ordered `efforts` array replaces the `minLevel`/`maxLevel`/`levels` range encoding, and the wire facts are baked alongside it — `effortMap` (anthropic-adaptive 4-tier vs 5-tier scale, shared with the OpenRouter completions remap) and `supportsDisplay` (adaptive `display` field support). Explicit spec thinking owns the capability surface (`mode`/`efforts`/`defaultLevel`) and wins over inference; missing wire facts are backfilled from identity so configs never need to know Anthropic's tier tables. Reasoning models that reject the wire effort param (`compat.supportsReasoningEffort: false` on openai-responses*) are encoded as `thinking: undefined` (\"thinks, no control surface\") instead of the removed `modelOmitsReasoningEffort` special case. `models.json` was re-baked in the new vocabulary behind a 3196-model behavioral parity gate, and the model cache schema bumped to v4 to invalidate old-shape rows.\n- `mapEffortToGoogleThinkingLevel(effort)` is now a static map (model parameter dropped — validation stays at the `requireSupportedEffort` call sites), and `mapEffortToAnthropicAdaptiveEffort` reads the baked `thinking.effortMap` instead of re-classifying the model id per request.\n- Generator-only policy code moved out of the runtime bundle into `scripts/generated-policies.ts`: `applyGeneratedModelPolicies` (now policy fixups + thinking re-bake via the shared deriver), `linkOpenAIPromotionTargets`, the Copilot context-window table, minimax/opencode-go compat fixups, and `CLOUDFLARE_FALLBACK_MODEL`. The anthropic id predicates (`hasOpus47ApiRestrictions`, `supportsMidConversationSystemMessages`, `isAnthropicFableOrMythosModel`) moved to `identity/family` for build-time use by the compat/thinking derivers only.\n\n### Fixed\n\n- Fixed Anthropic official-endpoint detection to require strict HTTPS hostname matching so non-official or lookalike URLs are no longer treated as official Anthropic hosts\n- Fixed Ollama Cloud dynamic discovery so same-id matches from other providers no longer supply context-window or max-output-token limits for discovered models.\n- Wired `@oh-my-pi/pi-catalog` into the release publish package list, tarball install smoke test, and root `bun generate-models` script.\n- Fixed `supportsAdaptiveThinkingDisplay` only matching dash-form version ids: dotted ids (`claude-opus-4.7`) now classify through `identity/classify` like every other anthropic predicate, so six bundled dotted Opus 4.7/4.8 entries (github-copilot, vercel-ai-gateway, zenmux) regain adaptive `display` support; bare dated ids (`claude-opus-4-20250514` = Opus 4.0) stay excluded.\n- Fixed the OpenRouter anthropic adaptive-effort map misclassifying bare dated Opus ids (`claude-opus-4-20250514` parsed as version 4.20 → wrongly adaptive); the map now derives from the shared classifier and the shared 4-/5-tier tables.\n\n### Removed\n\n- Removed the runtime enrichment layer: `enrichModelThinking` (and its non-enumerable memo-slot cache), `refreshModelThinking`, `modelOmitsReasoningEffort`, and the `model-thinking` re-exports of generator-only policies. Thinking metadata is resolved exactly once inside `buildModel`; runtime helpers (`getSupportedEfforts`, `clampThinkingLevelForModel`, `requireSupportedEffort`, the effort mappers) are pure field reads.\n"}, "003bb5548c7c1e759336e264305790de435708d1": {"t": 1785846856.3916426, "n": 81275, "h": "8af493ec287f36ba", "c": "# Changelog\n\n## [Unreleased]\n\n## [17.2.7] - 2026-08-03\n\n### Fixed\n\n- Fixed an issue where setting `thinking-level: off` failed to disable reasoning on direct DeepSeek V4 requests.\n\n## [17.2.6] - 2026-08-03\n\n### Added\n\n- Added the `bedrock-mantle` provider with authenticated model discovery for OpenAI GPT-5.4, GPT-5.5, and GPT-5.6 models (including Luna and Terra variants with corrected pricing) served through Amazon Bedrock's Responses endpoint.\n\n### Fixed\n\n- Fixed dynamic discovery for the `deepseek-v4` model family (such as `deepseek-v4-flash-0731`) under `alibaba-token-plan` missing reasoning configuration and maximum thinking effort.\n- Fixed GitHub Copilot dynamic discovery retaining stale bundled prices for default-context models instead of using the provider's reported default-tier prices.\n\n## [17.2.5] - 2026-08-03\n\n### Fixed\n\n- Fixed an issue where newly advertised chat models were dropped during dynamic discovery for the `alibaba-token-plan` provider.\n- Fixed a `400` error when forcing a specific tool with DeepSeek reasoning models on OpenCode Zen/Go gateways by automatically downgrading the tool selection mode to `auto` while keeping the tool advertised.\n\n## [17.2.4] - 2026-08-01\n\n### Added\n\n- Added `AnthropicCompat.streamIdleTimeoutMs` and propagated it through `buildAnthropicCompat` so direct Anthropic provider streams can configure their inter-event idle watchdog.\n- Fixed Ollama Cloud DeepSeek V4 Pro/Flash models (including dated tag variants such as `deepseek-v4-flash:0731`) reporting an incorrect max-output-tokens figure by pinning it to the deployment's enforced 65536-token output ceiling ([#7266](https://github.com/can1357/oh-my-pi/issues/7266)).\n\n### Fixed\n\n- Fixed `gen:models` Codex discovery to union models across every stored OAuth account and fail closed on partial resolution, matching runtime discovery ([#6265](https://github.com/can1357/oh-my-pi/issues/6265)); restored the bundled `gpt-5.4`, `gpt-5.6-sol`, and `gpt-5.3-codex-spark` entries a single-account regen had dropped.\n- Fixed `google-antigravity` models always reporting $0 cost: Antigravity discovery carries no pricing, so the generator now back-fills each model with its Google list price (Gemini ids from the `google` provider, including `-preview` id aliases; Claude ids from `google-vertex`, falling back to `anthropic`).\n- Fixed OpenRouter `deepseek/deepseek-v4-flash-0731` exposing only `high` thinking effort by consuming the live `reasoning.supported_efforts` and `default_effort` metadata and bundling its `low`/`high`/`max` ladder. ([#7307](https://github.com/can1357/oh-my-pi/issues/7307))\n\n## [17.2.3] - 2026-08-01\n\n### Added\n\n- Added support for the ai& provider (`aiand`), an OpenAI-compatible inference API with dynamic model discovery (context windows, capabilities, reasoning efforts, and USD pricing from `/v1/models`) and API-key authentication via the `AIAND_API_KEY` environment variable.\n\n## [17.2.2] - 2026-07-31\n\n### Added\n\n- Added support for the GMI Cloud provider (`gmi-cloud`), an OpenAI-compatible inference gateway with dynamic model discovery and API-key authentication via the `GMI_API_KEY` environment variable.\n- Added optional authoritative context occupancy to usage records for providers with separate checkpoint telemetry and billable token buckets.\n\n### Fixed\n\n- Fixed classification of dynamically discovered Cursor Kimi K3 effort variants as non-reasoning models when `thinkingDetails` is omitted.\n- Fixed Google AI Studio OpenAI-compatible requests failing with HTTP 400 by omitting the unsupported `store` field.\n- Fixed Synthetic models losing capabilities (such as reasoning/thinking selectors, vision input, output limits, and pricing) by correcting how the discovery mapper parses Synthetic's advertised features, effort vocabularies, and pricing structures.\n- Fixed Cursor model discovery to correctly expose the 1M-token context window for supported models (including Claude, GPT, Kimi K3, and GLM 5.2+ families) instead of defaulting to 200k.\n- Fixed GitHub Copilot routing for `grok-4.5` to use the correct Responses endpoint instead of the unsupported Chat Completions endpoint.\n\n## [17.2.1] - 2026-07-30\n\n### Fixed\n\n- Fixed Ollama model-manager caches being reused after the configured base URL changed by scoping cache namespaces to the normalized native discovery endpoint, including reverse-proxy path prefixes ([#7087](https://github.com/can1357/oh-my-pi/issues/7087)).\n\n## [17.2.0] - 2026-07-30\n\n### Added\n\n- Regenerated the Cursor agent protobufs (`discovery/cursor-gen/agent_pb.ts`) against the modern `agent.proto`, adding the message and enum families current Cursor CLI builds emit: Pi tool exec frames, hook queries and responses, subagents, allowlist prechecks, MCP state, smart-mode classification, canvas diagnostics, conversation search, agent-store conflicts and git diff. Purely additive — no existing exported symbol changed shape.\n\n### Fixed\n\n- Fixed an issue where LM Studio first turns failed with a 400 Invalid tool_choice error when a named tool was forced, by using the supported tool_choice: \"required\" string selector.\n\n## [17.1.8] - 2026-07-28\n\n### Added\n\n- Added `resolveVertexEndpointHost(location)` utility to resolve the correct Vertex AI API endpoint hostnames for global, multi-region, and regional locations.\n\n### Fixed\n\n- Fixed an issue where `calculateCost` under-reported Anthropic cache-write costs by honoring the `usage.cttl` breakdown to correctly price 1-hour retention writes at 2x the base input rate.\n\n## [17.1.7] - 2026-07-27\n\n### Added\n\n- Added support for moonshotai/Kimi-K3 and kimi-k3-fast models\n- Added umans-kimi-k3 prerelease model configuration\n\n### Changed\n\n- Updated pricing and token limits for selected models\n\n## [17.1.6] - 2026-07-27\n\n### Added\n\n- Added SiliconFlow providers (`siliconflow`, `siliconflow-cn`) with dynamic-only OpenAI-compatible model discovery: no bundled catalog — the model list is fetched live from each region's `/v1/models` endpoint, with non-chat entries (embedding, reranker, image, audio, video) filtered out. Discovery hydrates pricing, context/output limits, and reasoning metadata from the provider's models.dev catalog at runtime (with bundled upstream references as a reasoning-only fallback for ids models.dev has not indexed), so reasoning models keep thinking enabled and sessions compact against real context windows. `SILICONFLOW_API_KEY` / `SILICONFLOW_CN_API_KEY` environment variables are wired into `getEnvApiKey`.\n\n## [17.1.5] - 2026-07-27\n\n### Fixed\n\n- Fixed Kimi Code (`kimi-code`) reporting `maxTokens: 32000` for every model — its `/coding/v1/models` discovery mapper and the bundled catalog applied a blanket constant, truncating `k3`/`k3-256k` output at ~4x below their real 131072 ceiling and `kimi-for-coding`/`kimi-for-coding-highspeed` below their 32768 ceiling. Output caps are now derived per family, and the model cache is invalidated so upgrades drop the stale `maxTokens: 32000` rows (including the discovery-only `k3-256k`) instead of serving them until the next network refresh ([#6711](https://github.com/can1357/oh-my-pi/issues/6711)).\n- Fixed Anthropic model discovery 404ing when the registry derived the provider base URL from a bundled model without the `/v1` suffix (`https://api.anthropic.com/models` instead of `/v1/models`), which let a stale text-only cache row shadow fresh models.dev vision metadata — surfacing as snapcompact refusing to run on `claude-opus-5`. Discovery now always targets `/v1/models` while model rows keep the provider base URL ([#6563](https://github.com/can1357/oh-my-pi/issues/6563)).\n\n## [17.1.4] - 2026-07-26\n\n### Added\n\n- Added Claude Opus 5 model entries for Amazon Bedrock: `anthropic.claude-opus-5` plus its `us.`, `eu.`, `au.`, and `global.` regional/geo IDs.\n\n### Fixed\n\n- Fixed `alibaba-token-plan` locking out China (Beijing) 百炼 Token Plan subscribers: the provider hardcoded the international Singapore endpoint, so Beijing-issued `sk-sp-` keys got `401 invalid_api_key`. The wire credential now carries an optional region base URL, and model discovery targets the credential's region ([#6682](https://github.com/can1357/oh-my-pi/issues/6682)).\n- Fixed forced `tool_choice` 400s (`tool_choice 'specified' is incompatible with thinking enabled`) on Kimi Code's Anthropic-compatible endpoint for the `kimi-for-coding`, `kimi-for-coding-highspeed`, and `k3` aliases: the Anthropic-surface compat matcher only recognised Moonshot's native `kimi-k2.7-code*` ids, so thinking-locked kimi-code models kept `supportsForcedToolChoice: true` and the forced selector was sent to a host that always thinks. These models now resolve `requiresThinkingEnabled`, keeping thinking on and downgrading forced choices to `auto`.\n- Retried empty successful provider discovery responses after the short non-authoritative interval instead of caching them for the full catalog TTL ([#6620](https://github.com/can1357/oh-my-pi/issues/6620)).\n- Fixed GitHub Copilot Claude models with no bundled catalog reference (e.g. a freshly served `claude-opus-5`) discovering with `reasoning: false`/`thinking: null` and no effort dial, and disappearing along with their synthesized `-1m` sibling on offline reads: reference-less Copilot models on the anthropic-messages proxy now derive the adaptive reasoning ladder from the model id, and the cache restores their compile-time `COPILOT_API_HEADERS` by value instead of dropping them as unrestorable ([#6664](https://github.com/can1357/oh-my-pi/issues/6664)).\n\n## [17.1.3] - 2026-07-24\n\n### Fixed\n\n- Disabled the first-event watchdog for local OpenAI-compatible backends while retaining the 300-second inter-event watchdog, so long llama.cpp prompt prefill is not canceled and retried ([#6524](https://github.com/can1357/oh-my-pi/issues/6524)).\n\n## [17.1.1] - 2026-07-24\n\n### Added\n\n- Added catalog metadata for models that support native computer-use requests.\n- Added resolved Bedrock Converse prompt-cache compatibility limits, including explicit 5-minute checkpoint support for bundled Nova Lite, Micro, Pro, Premier, and Nova 2 Lite models plus their documented in-region, regional, and global IDs, and model-specific 1-hour Claude retention.\n- Added resolved Bedrock Converse prompt-cache compatibility limits, including explicit 5-minute checkpoint support for bundled Nova Lite, Micro, Pro, and Premier models plus Nova Premier's documented in-region model ID, and model-specific 1-hour Claude retention.\n- Added the native Meta Model API provider and Muse Spark 1.1 with Responses API reasoning replay, image input, and the full supported reasoning-effort ladder ([#4941](https://github.com/can1357/oh-my-pi/issues/4941)).\n- Added an opt-in Vercel AI Gateway automatic prompt-cache compatibility option alongside provider routing preferences.\n- Added Vercel AI Gateway Responses cache-anchor and cache-lifetime compatibility controls.\n- Added resolved OpenAI GPT-5.6 prompt-cache breakpoint capability metadata, keeping older models and compatible endpoints opt-in only.\n- Added the native `alibaba-token-plan` provider with QwenCloud Token Plan Individual discovery and a curated chat-model fallback catalog ([#6151](https://github.com/can1357/oh-my-pi/issues/6151)).\n\n## [17.1.0] - 2026-07-24\n\n### Added\n\n- Added Bedrock Converse prompt-cache compatibility limits, including 5-minute checkpoint support for Nova models (Lite, Micro, Pro, Premier, Nova 2 Lite) and 1-hour Claude retention.\n- Added native Meta Model API provider and Muse Spark 1.1 support, featuring Responses API reasoning replay, image input, and reasoning-effort controls.\n- Added Vercel AI Gateway integration features, including opt-in automatic prompt-cache compatibility, provider routing preferences, and Responses cache-anchor and cache-lifetime controls.\n- Added prompt-cache breakpoint capability metadata for OpenAI GPT-5.6, with opt-in support for older models and compatible endpoints.\n- Added native alibaba-token-plan provider with QwenCloud Token Plan Individual discovery and a curated chat-model fallback catalog.\n\n## [17.0.9] - 2026-07-23\n\n### Changed\n\n- Renamed `codex-auto-review` model to `GPT-5.3 Codex Spark` with updated pricing and capabilities\n- Removed image input support from GPT-5.3 Codex Spark (text-only)\n- Reduced GPT-5.3 Codex Spark context window from 272K to 128K tokens\n- Changed GPT-5.3 Codex Spark thinking efforts from `[\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"]` to `[\"low\", \"medium\", \"high\", \"xhigh\"]`\n- Updated pricing for multiple AI models across providers (costs adjusted in models.json)\n- Reduced max output tokens for an unspecified model from 16384 to 8192\n- Added image input support to Venice AI text model\n\n## [17.0.8] - 2026-07-22\n\n### Added\n\n- Added support for several new models across multiple providers, including MiniMax M3, Gemini 3.5 Flash Lite, Gemini 3.6 Flash (with thinking support), Hy3, Doubao-Seed-Character, LongCat 2.0, Laguna S 2.1 (free and paid tiers), Qwen 3.6 35B A3B, SWE-1.6 Slow (devin agent catalog), and XiaomiMiMo/MiMo-V2.5.\n\n### Changed\n\n- Updated Grok 4.5 API type to \"openai-responses\" and updated \"o3-mini\" to support thinking capabilities with the \"kimi\" thinking format.\n- Renamed OpenRouter-specific models and routers to include \"OpenRouter\" in their names (e.g., \"OpenRouter Auto Router (Beta)\", \"OpenRouter Body Builder (beta)\", and \"OpenRouter Pareto Code Router\").\n- Updated context window sizes, costs, and token limits for numerous models.\n\n### Fixed\n\n- Fixed an issue where GPT-5.6 Codex SKUs lost usable context window capacity due to dynamic discovery values overwriting bundled limits.\n- Fixed OpenAI Codex discovery dropping account-listed ChatGPT-only models (such as GPT-5.3 Codex Spark) when they are unavailable through the public API.\n- Fixed Codex catalog discovery hiding models when multiple OAuth accounts are configured by independently fetching and merging catalogs from all accounts.\n- Fixed cached models reusing a bundled request model (such as GitHub Copilot long-context variants) being incorrectly flagged as unrestorable and dropped after a restart.\n- Fixed LM Studio discovery reporting a model's theoretical maximum context length instead of the actual loaded context window size of the running instance.\n\n### Removed\n\n- Removed several deprecated model families from the devin catalog, including Claude Fable 5, Claude Opus 4.6/4.7, Claude Sonnet 4.6/5, DeepSeek V4 Pro, Gemini 3.1 Pro, Gemini 3.5 Flash, GLM-5.2, SWE-1.6, and Nemotron 3 Ultra.\n- Removed GPT-5 through GPT-5.3 Codex variants and GPT-5.4 nano from the openai-codex catalog.\n\n## [17.0.6] - 2026-07-20\n\n### Added\n\n- Added static fallback seed for Devin's `swe-1-7` model so it is bundled even when catalog generation runs without a Devin session token.\n\n### Fixed\n\n- Collapsed Devin's six GLM-5.2 variants into two logical entries (`glm-5-2` for 200K free, `glm-5-2-1m` for 1M paid). The 200K entry routes every thinking effort to the free `glm-5-2` wire UID — never to the quota-gated `glm-5-2-max` or `glm-5-2-none` — so GLM-5.2 works even when the weekly usage quota is exhausted.\n\n## [17.0.5] - 2026-07-18\n\n### Added\n\n- Added an Anthropic compatibility flag to allow non-official OAuth endpoints to opt into configured Claude Code fingerprint header overrides.\n\n### Fixed\n\n- Fixed a security issue where sensitive provider-defined request headers (such as API keys or credentials) were serialized in plaintext within the model cache (models.db). The cache now omits these headers, securely invalidates older cached rows, and restores or refetches them dynamically.\n- Fixed OpenAI Codex discovery to respect caller-supplied fetch configurations (such as proxies or custom CAs) and correctly replace stale bundled models with the authenticated account catalog.\n- Fixed stream timeouts and retry loops during long prefills on local loopback or RFC1918 backends (such as litellm proxies fronting local servers) by applying the local stream-timeout floor to these backends.\n- Fixed Kimi K3 models served through generic OpenAI-compatible routes exposing unsupported reasoning efforts instead of the mandatory low/high/max scale.\n\n## [17.0.4] - 2026-07-18\n\n### Changed\n\n- Kimi-family models now use MFJS tool schema on all hosts, including proxies like OpenRouter that forward schemas to Moonshot\n\n## [17.0.3] - 2026-07-17\n\n### Fixed\n\n- Logged LiteLLM rich-metadata endpoint failures once with their endpoint and status before falling back to incomplete `/v1/models` data ([#5801](https://github.com/can1357/oh-my-pi/issues/5801)).\n- Fixed authenticated Kimi Code discovery to preserve live effort levels, default effort, mandatory-thinking state, and per-model protocol metadata ([#5893](https://github.com/can1357/oh-my-pi/issues/5893)).\n- Fixed LiteLLM provider ignoring per-model pricing: `mapLiteLLMRichEntry` now reads `input_cost_per_token` / `output_cost_per_token` (plus cache costs) from LiteLLM rich metadata and maps them to `cost.input` / `cost.output`, falling back to the bundled reference only when LiteLLM omits cost, so proxied models no longer display as free ([#5818](https://github.com/can1357/oh-my-pi/issues/5818)).\n\n## [17.0.2] - 2026-07-17\n\n### Changed\n\n- Increased the maximum output tokens (maxTokens) from 32,768 to 65,536 for Kimi K2.7-Code models on Fireworks.\n\n### Fixed\n\n- Fixed a regression where the context window for openai-codex GPT-5.6 models (Luna, Sol, Terra) incorrectly fell back to 272,000 instead of preserving its 372,000 capacity.\n- Fixed Umans PAYG models incorrectly displaying as \"Free\" in /models by correctly sourcing their published per-token rates.\n- Fixed native moonshot/kimi-k3 capabilities and pricing, ensuring it correctly reflects its official pricing, 1M context window, image input support, reasoning capabilities, and 128k output token limit.\n\n## [17.0.1] - 2026-07-16\n\n### Added\n\n- Added GPT-5.6 Luna, Sol, and Terra entries for Amazon Bedrock, Azure, and Cloudflare\n- Added KAT-Coder Air/Pro V2.5 entries across Kilo, OpenRouter, NanoGPT, and Vercel\n- Added Inkling model entries for Baseten and Vercel AI Gateway\n- Added Umans DeepSeek V4 Pro DSpark as an experimental model listing\n- Added Claude Opus 4.7 Fast and 4.8 Fast on Vercel AI Gateway\n- Added Workers AI GLM-5.2, Muse Spark 1.1, Stealth GPT-5.6 Sol, and nano-gpt-help entries\n\n### Changed\n\n- Added image input and reasoning support to several existing Codeium and Kilo GPT-5.6 models\n- Enabled image input and reasoning for Gemini Flash Latest and Grok 4.5\n- Renamed many model labels for consistency, including Claude, Grok, DeepSeek, GLM, and Gemi­ni names\n- Updated pricing for many existing models, including input, output, and cache cost values\n- Updated context window and max token limits for many catalog models across providers\n\n### Fixed\n\n- Fixed Z.AI (GLM) coding-plan token costs all showing as \"Free\" in `/models`: the `zai` provider descriptor sourced the models.dev `zai-coding-plan` key (all-$0 subscription rates) instead of the `zai` pay-as-you-go key, which carries the real per-token rates for the identical GLM ids ([#5598](https://github.com/can1357/oh-my-pi/issues/5598)).\n- Fixed custom Anthropic endpoints receiving the first-party-only `eager_input_streaming` tool field by default ([#5572](https://github.com/can1357/oh-my-pi/issues/5572)).\n- Added resolved OpenAI sampling-parameter compatibility metadata for o-series and GPT-5+ models.\n- Fixed GitHub Copilot `mai-code-1-flash-picker` (and other `mai-*` models) to route through the `/responses` endpoint instead of `/chat/completions`, which rejected them with `400 unsupported_api_for_model` ([#5612](https://github.com/can1357/oh-my-pi/issues/5612)).\n- Extended the reasoning `streamIdleTimeoutMs` floor (300s) to native Kimi K2.7 Code (`kimi-k2.7-code` / `kimi-k2.7-code-highspeed`), which previously fell through to the 120s default and aborted on long reasoning turns ([#4836](https://github.com/can1357/oh-my-pi/issues/4836)).\n- Fixed GLM-5.x coding-plan streams via the OpenCode Go/Zen gateways (`opencode.ai/zen/…`) timing out with `OpenAI completions stream stalled while waiting for the next event` during slow plan-writing/reasoning phases. The 600s idle-timeout floor for GLM coding-plan SKUs was gated to the native Z.AI/Zhipu hosts only, so OpenCode-fronted GLM fell back to the 120s default watchdog. ([#4758](https://github.com/can1357/oh-my-pi/issues/4758))\n\n## [16.5.2] - 2026-07-14\n\n### Fixed\n\n- Fixed OpenCode Zen and Go discovery to replace stale bundled models with each provider's live model catalog.\n\n## [16.5.1] - 2026-07-14\n\n### Fixed\n\n- Fixed reasoning effort mapping for Z.ai GLM-5.2 on the Anthropic messages endpoint to correctly use the two-tier scale (high, max) and emit output_config.effort.\n- Fixed an issue where stale cached model limits would override updated static catalog limits after a catalog fingerprint mismatch.\n- Fixed Cursor discovery to correctly preserve GetUsableModels max-mode metadata for premium models and invalidate stale cache entries.\n\n## [16.4.3] - 2026-07-11\n\n### Fixed\n\n- Fixed parsing of SAP AI Core Claude model IDs in version-first format (e.g., anthropic--claude-4.8-opus), restoring adaptive thinking metadata and capability gates.\n- Fixed GitHub Copilot Business and Enterprise model discovery to correctly preserve vision capabilities instead of downgrading models to text-only.\n\n## [16.4.2] - 2026-07-10\n\n### Fixed\n\n- Fixed OpenAI Codex model discovery to include the Codex version header alongside the client_version query parameter.\n\n## [16.4.1] - 2026-07-10\n\n### Added\n\n- Added GPT-5.6 Luna, Sol, and Terra models\n- Added perplexity-academic-researcher model\n\n### Changed\n\n- Updated context windows for multiple GPT-5.6 models\n- Increased max tokens for several models\n- Updated cache write costs for GPT-5.6 variants\n- Reduced pricing for select models\n\n### Removed\n\n- Removed the generated GPT-5.6 pro-reasoning aliases (`gpt-5.6-{luna,sol,terra}-pro`) from the `openai-codex` subscription provider — pro reasoning is not offered on subscriptions; the `openai` API-key aliases remain\n\n## [16.4.0] - 2026-07-10\n\n### Breaking Changes\n\n- Redesigned reasoning effort ladders to be wire-exact, removing the shifted five-tier effort mapping. Models now expose exactly the effort tiers their upstream APIs accept, mapped 1:1. Removed SHIFTED_FIVE_TIER_EFFORT_MAP, ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER, and per-host xhigh-to-max alias maps. Selecting an unsupported tier now automatically clamps down via clampThinkingLevelForModel. Devin effort routing is now mapped 1:1 onto per-tier siblings.\n\n### Added\n\n- Added support for new models: Grok 4.5 family, Dolphin Mistral 24b Venice Edition, GLM5.2-Fast, and Zenmux variants for GPT-5.6 (Luna, Sol, and Terra).\n- Added Novita as a model provider, including public catalog discovery, pricing, limits, modality, reasoning, and tool metadata.\n- Added useResponsesLite to Model and ModelSpec to support the Responses Lite transport, enabled by default for the GPT-5.6 family.\n- Added Effort.Max (\"max\") as a first-class user-facing thinking level above xhigh.\n\n### Changed\n\n- Enabled reasoning effort controls for Grok 4.5 and updated support flags for additional Grok variants\n- Standardized reasoning effort levels to use a wire-exact max tier across all model providers, including Devin routing and Ollama configurations.\n- Updated costs and context windows for various models in the catalog.\n\n## [16.3.15] - 2026-07-09\n\n### Added\n\n- Added support for Grok 4.5 model\n- Added `gpt-5.6` base models and `gpt-5.6-{luna,sol,terra}-pro` variants\n- Added `meta/muse-spark-1.1` model support\n- Added support for thinking modes on `poolside/laguna` models\n- Added generated GPT-5.6 Pro aliases (`gpt-5.6-{luna,sol,terra}-pro`) on the `openai` and `openai-codex` providers: each alias sends the base model id on the wire (`requestModelId`) with the new `reasoningMode: \"pro\"` marker, and re-derives from the current base rows on every catalog regeneration.\n\n### Changed\n\n- Updated cache read costs for Grok models\n- Reduced max token limit for Grok 4.3 model\n- Enabled prompt cache affinity for Grok models via the x-grok-conv-id header in OpenAI compatible endpoints\n- Enabled prompt cache affinity for Grok models via the x-grok-conv-id header\n- Marked direct xAI Grok Chat Completions models for `x-grok-conv-id` prompt-cache affinity.\n\n## [16.3.14] - 2026-07-09\n\n### Added\n\n- Added support for GPT-5.6 (Luna, Sol, Terra) model variants\n- Enabled expanded five-tier reasoning effort scale (minimal to xhigh) for GPT-5.6 models\n- Added GPT-5.6 (Terra/Luna/Sol) support for the new `max` reasoning tier: on wire-effort APIs (OpenAI Responses, Codex, Azure, openai-compat/OpenRouter models that advertise reasoning) user efforts shift up one notch — `xhigh` sends `max`, `high` sends `xhigh` — mirroring the Claude Fable/Opus 4.7+ five-tier mapping, and the exposed ladder becomes `minimal..xhigh` with `minimal` reaching the native `low` tier. Devin's per-tier GPT-5.6 sibling rows now collapse into `gpt-5-6-{luna,sol,terra}` logical models with the same shifted routing (`xhigh` → `-max`), plus `-fast` families that keep the direct `low..xhigh` `-priority` scale since Devin serves no `-max-priority` tier.\n\n## [16.3.13] - 2026-07-09\n\n### Added\n\n- Added support for Grok 4.5 across multiple providers\n- Added support for GPT-5.6 series models (Luna, Sol, Terra)\n- Added Aion 3.0 and 3.0 Mini models\n- Added Kuaishou KAT-Coder v2.5 models\n- Added Nex-N2-Mini and SWE-1.7 series models\n- Added Hy3 models and free variants\n\n### Changed\n\n- Updated cost and token configurations for various models across providers\n- Renamed several models for consistency (e.g., MiniMax M3, Gemma 4 31B, Qwen variants)\n\n## [16.3.12] - 2026-07-08\n\n### Fixed\n\n- Fixed LiteLLM discovery stopping at `/model_group/info` when that endpoint omitted `supports_vision`; it now continues to `/model/info` and preserves `model_info.supports_vision=true` for vision-capable proxy models. ([#4747](https://github.com/can1357/oh-my-pi/issues/4747))\n- Fixed LiteLLM discovery to fall back to bundled catalog metadata when `models.dev` lacks a model reference, preserving reasoning and thinking support for models such as `glm-5.2`. ([#4695](https://github.com/can1357/oh-my-pi/issues/4695))\n- Detected Azure AI Inference / Foundry Anthropic routes as strict-tool-incompatible so resolved Anthropic compat disables strict tools before request construction ([#4679](https://github.com/can1357/oh-my-pi/issues/4679)).\n\n## [16.3.11] - 2026-07-06\n\n### Added\n\n- Added Claude Haiku 4.5 (JP) model support\n- Added tencent/hy3 model support via ZenMux\n\n### Changed\n\n- Updated naming format for various synthetic models to include provider prefix\n- Adjusted context window limit for MiniMax-M3 model\n- Updated pricing for select models\n\n## [16.3.10] - 2026-07-06\n\n### Fixed\n\n- Fixed LiteLLM rich discovery to ignore unusable sentinel placeholders and continue to `/v2/model/info` for real models. ([#4655](https://github.com/can1357/oh-my-pi/issues/4655))\n\n## [16.3.9] - 2026-07-06\n\n### Fixed\n\n- Fixed compatibility with OpenCode Go DeepSeek V4 models by sending max_tokens instead of max_completion_tokens to match the provider's API requirements.\n\n## [16.3.7] - 2026-07-05\n\n### Fixed\n\n- Fixed usage cost calculation to correctly account for provider orchestration token sidecars without misclassifying them as standard input, output, or cache tokens.\n\n## [16.3.4] - 2026-07-03\n\n### Added\n\n- Added Baseten as a supported model provider\n- Added support for new models from Baseten, including DeepSeek V4 Pro and Kimi series\n- Added new Devin agent models: Claude 5 Fable variants\n- Added new Github Copilot models: Kimi K2.7 Code and MAI-Code-1-Flash\n- Added Poolside Laguna XS 2.1 models via Kilo and OpenRouter providers\n- Added support for Claude Fable 5 (Free) via Zenmux provider\n\n### Changed\n\n- Updated priority ordering to include Baseten\n- Updated pricing and limits for various existing models in the catalog\n\n## [16.3.3] - 2026-07-02\n\n### Fixed\n\n- Extended Anthropic-compatible signing-endpoint recognition to Cloudflare AI Gateway, Google Vertex, AWS Bedrock, and Azure AI Inference / Foundry to ensure consistent reasoning-replay and signature-stripping behavior, and exposed ResolvedAnthropicCompat.signingEndpoint in the public API.\n- Fixed Zhipu Coding Plan runtime discovery to prioritize account-scoped model lists over bundled fallback models, preventing routing errors for valid non-z.ai keys.\n\n## [16.3.2] - 2026-07-02\n\n### Fixed\n\n- Fixed ZenMux model discovery to run without a `ZENMUX_API_KEY`, so newly published ZenMux models (for example `anthropic/claude-fable-5-free`) auto-update into the runtime `models.db` cache instead of waiting on a regenerated `models.json`.\n- Fixed ZenMux runtime discovery to query the `/api/v1/models` endpoint even when the resolved provider base URL points at the Anthropic-compatible route, so discovery no longer requests a non-existent `/api/anthropic/models` path.\n\n## [16.3.1] - 2026-07-02\n\n### Removed\n\n- Removed reasoning suppression prompt logic for GPT-5 models\n\n## [16.3.0] - 2026-07-02\n\n### Breaking Changes\n\n- Renamed the `requiresJuiceZeroHack` compatibility flag to `requiresReasoningSuppressionPrompt` (affecting `OpenAICompat` and `ResolvedOpenAIResponsesCompat`) and removed the unused `\"juice-zero-developer-message\"` member from `OpenAIReasoningDisableMode`.\n\n### Fixed\n\n- Fixed stream markup healing pattern misfires by disabling the healer on the official OpenAI endpoint.\n- Updated the Xiaomi provider's default model to the supported `mimo-v2.5` model.\n- Fixed model discovery probes (including Ollama and metadata fetches) failing behind private-CA gateways by ensuring they honor the `NODE_EXTRA_CA_CERTS` environment variable.\n- Fixed CoreWeave Serverless Inference project-header detection to ensure blank OpenAI-Project overrides do not block the `COREWEAVE_PROJECT` fallback.\n- Fixed LiteLLM MiniMax M3 discovery to remove reseller-only display suffixes and invalidated the model cache to clear stale suffixes immediately.\n- Fixed ZenMux's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`), matching the GitHub Copilot fix (#2851). ZenMux's `zenmux.ai/api/anthropic` route forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: \"\"` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused `400 messages.1.content.0: Invalid signature in thinking` on Claude Sonnet 5 and other reasoning models. ([#4192](https://github.com/can1357/oh-my-pi/issues/4192))\n\n## [16.2.13] - 2026-07-01\n\n### Added\n\n- Added support for human-readable reasoning summaries on compatible OpenAI Codex models (v5.4+)\n\n### Fixed\n\n- Fixed discovered OpenAI Codex models to advertise V2 streaming remote compaction, avoiding the legacy compact endpoint timeout path for Codex sessions. ([#4146](https://github.com/can1357/oh-my-pi/issues/4146))\n\n## [16.2.12] - 2026-07-01\n\n### Breaking Changes\n\n- Removed runtime canonical-equivalence APIs from the identity module, including resolveCanonicalVariant, buildCanonicalModelOrder, CanonicalVariantPreferences, and getBundledCanonicalReferenceData. These utilities have been transitioned to a build-time generator script and are no longer exposed in the runtime bundle.\n\n## [16.2.11] - 2026-07-01\n\n### Fixed\n\n- Fixed a potential memory leak caused by dangling timeout timers during model discovery in OpenAI-compatible, vLLM, LiteLLM, and LM Studio catalogs.\n- Widened stream watchdogs for local OpenAI-compatible backends (including llama.cpp, LM Studio, vLLM, and Ollama) to prevent premature timeouts during cold model loads.\n\n## [16.2.10] - 2026-06-30\n\n### Added\n\n- Added Claude Sonnet 3.7, Claude Opus 3, and Claude Sonnet 3 model entries to the Anthropic catalog\n- Added Anthropic Claude Sonnet 5 model entry to the Kilo provider catalog\n- Added first-party catalog discovery support for the Anthropic provider\n- Added Gemini 3.1 Flash Lite Image model entry to the Kilo provider catalog\n- Added Anthropic Claude Sonnet 5 model variants with low, medium, high, xhigh, and max thinking efforts to the Devin provider catalog\n- Added Claude Sonnet 5 model entry to the Anthropic curated catalog.\n\n### Changed\n\n- Updated the base API URL for the Claude Sonnet 5 model in the Anthropic catalog\n- Updated pricing metrics for DeepSeek R1 and DeepSeek V3 model entries to reflect new rates\n\n## [16.2.9] - 2026-06-30\n\n### Added\n\n- Added full capability support for Claude Sonnet 5, aligning it with Claude Opus 4.8 and Fable 5. This includes adaptive thinking display, mid-conversation system messages, sampling parameter and thinking omission API restrictions, and 5-tier adaptive reasoning effort mapping (including xhigh and max levels) across direct APIs, OpenRouter, and Bedrock Converse.\n\n### Changed\n\n- Updated input and output costs for models in the catalog.\n\n## [16.2.7] - 2026-06-30\n\n### Fixed\n\n- Fixed compatibility with Kimi K2.7 Code on native endpoints to ensure thinking mode is preserved and tool choice is not forced.\n- Fixed Cerebras gemma-4-31b dynamic discovery to correctly identify the model as image-capable, enabling proper serialization of attached images.\n\n## [16.2.6] - 2026-06-29\n\n### Fixed\n\n- Fixed namespaced GLM-5.x model IDs on Z.AI/Zhipu OpenAI-compatible endpoints to inherit the widened stream watchdog, avoiding spurious stalled-stream errors during long thinking phases. ([#3819](https://github.com/can1357/oh-my-pi/issues/3819))\n\n## [16.2.3] - 2026-06-28\n\n### Added\n\n- Added support and configuration parameters for V2 streaming compaction in RemoteCompactionConfig, catalog types, and model/provider metadata.\n\n### Changed\n\n- Enabled automatic content markup healing for all OpenAI-compatible streaming models\n- Updated pricing and context window limits for several catalog models.\n- Disabled reasoning capability for multiple providers in the catalog.\n\n## [16.2.2] - 2026-06-27\n\n### Removed\n\n- Removed 'pi' from the list of supported dialects.\n\n## [16.2.0] - 2026-06-27\n\n### Added\n\n- Added GitLab Duo Agent catalog discovery, including namespace selection, live model mapping, and a bundled fallback model for fresh installs.\n- Added OpenAICompat.supportsNamedToolChoice to support forced tool use on string-only OpenAI-compatible chat servers without emitting the named function-object tool_choice shape.\n- Added model metadata support for provider-native remote compaction and compaction-only model selection.\n\n### Changed\n\n- Disabled the thinking-effort selector for GitLab Duo Agent models since the underlying platform parameters are server-fixed.\n\n### Fixed\n\n- Improved GitLab Duo Agent and Duo Workflow namespace and project discovery to robustly handle paginated groups, SSH remotes with custom ports, Git worktrees, self-managed GitLab instances with relative paths, and configuration via GITLAB_DUO_PROJECT_PATH or GITLAB_DUO_PROJECT_ID.\n- Fixed built-in LiteLLM discovery to prefer rich proxy metadata from management endpoints and avoid caching stale capability data.\n- Fixed GitLab Duo Workflow model specifications to resolve correct static context windows, enabling accurate context usage tracking and auto-compaction.\n\n## [16.1.23] - 2026-06-26\n\n### Added\n\n- Added `OpenAICompat.qwenPreserveThinking` — auto-enabled when the resolved `thinkingFormat` is `\"qwen\"` or `\"qwen-chat-template\"` AND `replayReasoningContent` is on (i.e. the four built-in local OpenAI-compatible providers, or a custom provider pointed at a loopback / RFC1918 / `*.local` baseUrl). Pairs with the chat-completions encoder change so the request body carries `preserve_thinking: true` (twin top-level + `chat_template_kwargs` emission), keeping Qwen3.6+ from stripping `<think>...</think>` off older assistant turns and breaking the local slot's KV cache between user messages. Non-Qwen chat templates ignore the parameter, so the flag stays a no-op outside the Qwen path; users on a cloud Qwen host (Alibaba Dashscope / Qwen Portal) can opt in with `compat.qwenPreserveThinking: true`. ([#3541](https://github.com/can1357/oh-my-pi/issues/3541))\n- Added CoreWeave Serverless Inference as an OpenAI-compatible provider with models.dev-backed bundled catalog metadata.\n\n## [16.1.22] - 2026-06-26\n\n### Added\n\n- Added `OpenAICompat.replayReasoningContent` — auto-enabled for the built-in local OpenAI-compatible providers (`llama.cpp`, `lm-studio`, `vllm`, `ollama` on `openai-completions`) and for any provider pointed at a loopback / RFC1918 / `*.local` baseUrl. NOT gated on `spec.reasoning`: the runtime discovery paths for `llama.cpp` / `lm-studio` / `openai-models-list` hardcode `reasoning: false` because the upstream `/models` endpoints don't advertise the capability, while the stream parser still records incoming `reasoning_content` deltas as thinking blocks — gating on the spec flag would leave every discovered local Qwen / DeepSeek model re-triggering #3528. The encoder only writes `reasoning_content` when a thinking block actually exists on the turn, so the flag is a no-op on pure-text histories. Built-in proxy providers (currently `litellm`) are excluded from both checks because they forward to an unrelated upstream that gains no KV-cache benefit and may 400 on the extra field; users running a custom proxy in front of a llama.cpp-style backend can opt in via the sparse `compat.replayReasoningContent: true` override. Signals to the `openai-completions` encoder that preserved `thinking` blocks must be re-emitted as `reasoning_content` on every assistant turn so chat templates that reconstruct `<think>…</think>` from the field (Qwen3, DeepSeek-R1, GLM-5.x) keep the prior turn's tokens byte-stable and llama.cpp's prefix KV cache survives. ([#3528](https://github.com/can1357/oh-my-pi/issues/3528))\n\n## [16.1.20] - 2026-06-25\n\n### Fixed\n\n- Fixed direct Anthropic Claude Sonnet/Haiku 4.5 advisor/agent turns crashing every call with HTTP 400 `This model does not support the effort parameter.` The catalog classified the whole Claude 4.5 family on `anthropic-messages` (and `bedrock-converse-stream`) as `anthropic-budget-effort`, which made the Anthropic provider serialize `output_config.effort` alongside `thinking.budget_tokens`. Anthropic only honors `output_config.effort` on Opus 4.5 and adaptive (4.6+) Messages-API models, so Sonnet 4.5 / Haiku 4.5 rejected the field. `inferThinkingControlMode` now gates `anthropic-budget-effort` to `parsedModel.kind === \"opus\" && semverGte(version, \"4.5\")` on both Anthropic-routed APIs, so Sonnet 4.5 / Haiku 4.5 on direct Anthropic + Cloudflare-AI-Gateway + Vertex + GitLab-Duo + Copilot + Bedrock fall through to plain `mode: \"budget\"` (thinking budget still scales with the selected effort tier). Opus 4.5 keeps `anthropic-budget-effort`. `anthropic-budget-effort` also stays in use for Anthropic-compatible third-party backends that natively support the field (Umans GLM 5.2). ([#3497](https://github.com/can1357/oh-my-pi/issues/3497))\n\n## [16.1.17] - 2026-06-24\n\n### Fixed\n\n- Fixed the Umans GLM-5.2 thinking-level picker collapsing to a single `high` tier after dynamic discovery: the `max` upstream level now resolves to the internal `xhigh` effort, the picker shows both `high` and `xhigh`, and the metadata maps `xhigh` back to Umans's native `max` wire tier. ([#3192](https://github.com/can1357/oh-my-pi/issues/3192))\n- Fixed GitHub Copilot business and enterprise endpoints accepting image inputs that they reject with `400 vision is not supported`. The Copilot `/models` response advertises `capabilities.supports.vision = true` for Claude/GPT chat models on every host, but only the canonical personal endpoint (`https://api.githubcopilot.com`) actually serves them; `githubCopilotModelManagerOptions` now forces `input: [\"text\"]` whenever discovery resolves to a non-personal base URL, and `mergeDynamicModel` honours the dynamic value (instead of OR-upgrading) when the merged endpoint differs from the bundled reference. ([#3387](https://github.com/can1357/oh-my-pi/issues/3387))\n- Fixed OpenRouter Anthropic compat to strip Responses reasoning history during replay so signed thinking blocks are not sent back to routed Anthropic providers. ([#3399](https://github.com/can1357/oh-my-pi/issues/3399))\n\n## [16.1.14] - 2026-06-22\n\n### Added\n\n- Added Sakana AI provider support with Fugu model integration\n- Added Sakana AI/Fugu provider catalog entries with Fugu model discovery and Responses API metadata\n- Added support for \"xhigh\" reasoning tier across model configurations\n- Added configuration for new models GCP-5.4 Mini, GPT-5.5, and variants\n- Added `devin` variant collapse table to streamline model tiering\n\n### Changed\n\n- Updated reasoning label pattern to include \"minimal\" and \"max\" efforts\n- Simplified model identification logic for Devin-powered reasoning models\n- Refactored variant routing to consolidate and standardize tier definitions\n\n## [16.1.13] - 2026-06-22\n\n### Added\n\n- Added support for Devin as a model provider\n- Added capability to fetch dynamic models from the Devin model manager\n\n## [16.1.11] - 2026-06-21\n\n### Fixed\n\n- Fixed Umans `umans-glm-5.1` / `umans-glm-5.2` advertising native image input. The `models/info` endpoint reports `supports_vision: \"via-handoff\"` for the GLM models, meaning vision routes through a separate handoff pre-analysis step instead of accepting raw image blocks; `umansSupportsVision` treated any non-empty string as native vision support, so image prompts went directly to GLM and were rejected with `400 This model does not support image inputs`. The helper now requires `supports_vision === true`, the bundled GLM 5.1/5.2 rows are corrected to text-only, and stale mismatched Umans cache rows for those ids are dropped so the vision-handoff path runs even before a successful refresh. ([#3184](https://github.com/can1357/oh-my-pi/issues/3184))\n\n## [16.1.9] - 2026-06-21\n\n### Fixed\n\n- Fixed the `moonshot` provider with no path to the Kimi China API: model discovery now honors a `MOONSHOT_BASE_URL` override (redirecting to `api.moonshot.cn`), and `KIMI_API_KEY` resolves as a fallback for `MOONSHOT_API_KEY`. ([#2883](https://github.com/can1357/oh-my-pi/issues/2883))\n- Fixed LiteLLM model discovery preserving colliding models.dev transport metadata (for example `ollama-cloud` `deepseek-v4-flash`) instead of keeping the LiteLLM `openai-completions` provider transport. ([#3162](https://github.com/can1357/oh-my-pi/issues/3162))\n\n### Removed\n\n- Removed bundled Wafer Pass (`wafer-pass`) catalog entries and generation support; Wafer Serverless remains available as `wafer-serverless`.\n\n## [16.1.8] - 2026-06-20\n\n### Fixed\n\n- Fixed Fireworks-hosted Qwen turns (e.g. `fireworks/qwen3.7-plus`) failing with `400 Extra inputs are not permitted, field: 'enable_thinking'`. Fireworks serves Qwen3 with controllable thinking via OpenAI-style `reasoning_effort` and rejects the top-level `enable_thinking` boolean that Alibaba DashScope speaks; `buildOpenAICompat` was selecting `thinkingFormat: \"qwen\"` from the `qwen` id pattern regardless of host. Fireworks-hosted Qwen models now resolve to `thinkingFormat: \"openai\"`.\n- Fixed MiMo models on OpenAI-compatible gateways to expose only accepted `low`, `medium`, and `high` reasoning tiers and map unsupported raw `minimal`/`xhigh` requests to safe wire values. ([#2864](https://github.com/can1357/oh-my-pi/issues/2864))\n\n## [16.1.7] - 2026-06-20\n\n### Fixed\n\n- Fixed MiniMax-M3 catalog context for the MiniMax Coding/Token Plan providers `minimax-code` and `minimax-code-cn` to report the documented 1M long-context tier instead of the upstream 512K pricing boundary; the previous patch only covered `minimax`/`minimax-cn`, so the Coding Plan picker still showed 512K in the status bar ([#3097](https://github.com/can1357/oh-my-pi/issues/3097)).\n\n## [16.1.4] - 2026-06-19\n\n### Fixed\n\n- Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families so every effort and off resolve to the live wire id. Added `claude-sonnet-4-6` and `claude-opus-4-6-thinking` entries to `ANTIGRAVITY_MODEL_WIRE_PROFILES` capped at the backend's 64000-output-token limit (over-cap requests 400'd with `Request contains an invalid argument`); `modelEnum` is now optional on `AntigravityModelWireProfile` since the Claude wire ids are accepted without a captured `labels.model_enum`. ([#3067](https://github.com/can1357/oh-my-pi/issues/3067))\n\n## [16.1.3] - 2026-06-19\n\n### Fixed\n\n- Marked Ollama Cloud catalog models to omit on-the-wire output-token caps, preventing context-window-sized `num_predict` values from causing HTTP 400s for models whose true output cap is not discoverable. ([#2984](https://github.com/can1357/oh-my-pi/issues/2984))\n- Fixed `readModelCache`/`writeModelCache` using a process-global shared database even when a custom `dbPath` was provided. Custom-path cache operations now open and close a per-call database via `withModelCacheDb`, preventing leaked SQLite handles on Windows\n\n## [16.1.2] - 2026-06-19\n\n### Added\n\n- Added support for Gemini 2.5 Flash-Lite, 3.1 Flash-Lite, and 3.5 Flash models\n- Added support for Moonshot V1 model family\n\n### Changed\n\n- Updated context window and token limits for various Claude, Gemini, and GPT-OSS models\n- Refined thinking mode behaviors and routing for supported LLM families\n\n### Fixed\n\n- Fixed GLM-5.2 `reasoning_effort` so the top thinking tier reaches each host's genuine maximum instead of 400ing, mapping the internal `xhigh` tier per host dialect (verified against live endpoints): Z.ai/Zhipu collapse onto the model's `none`/`high`/`max` scale (`xhigh → max`); Fireworks, resellers, and Ollama Cloud keep their distinct lower tiers and remap only the top `xhigh → max` (merged over host quirks such as Fireworks' `minimal → none`); and OpenRouter — whose API rejects `max` and treats `xhigh` as its own max tier — now exposes the `xhigh` tier and forwards it verbatim. Dialect detection keys off resolved `compat.thinkingFormat`, so custom OpenRouter/Z.ai-format providers are covered too.\n- Maintained thinking effort routing when discovery only returns the base model ID\n- Improved credential retrieval logic for Antigravity and Codex providers via auth discovery\n\n## [16.0.9] - 2026-06-18\n\n### Fixed\n\n- Fixed GitHub Copilot's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`). It forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: \"\"` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused a `400 Invalid signature` that corrupted the session and re-tripped on every full history re-send (e.g. after toggling MCP servers). Copilot now degrades such blocks to text like the official API. ([#2851](https://github.com/can1357/oh-my-pi/issues/2851))\n- Added a `supportsImageDetailOriginal` compat flag that resolves to `false` for GitHub Copilot, whose Responses endpoint rejects the `detail: \"original\"` image hint with a 400, and `true` for every other host. ([#2822](https://github.com/can1357/oh-my-pi/issues/2822))\n\n## [16.0.8] - 2026-06-18\n\n### Changed\n\n- Refactored model family ID predicates and capability checkers to use a shared, uniform process-lifetime `memo` utility to eliminate caching boilerplate.\n\n### Fixed\n\n- Fixed LM Studio dynamic discovery to use native `/api/v0/models` metadata so VLM models advertise image input. ([#2945](https://github.com/can1357/oh-my-pi/issues/2945))\n\n## [16.0.7] - 2026-06-18\n\n### Fixed\n\n- Fixed MiniMax Anthropic-compatible M2/M3 thinking metadata to expose the adaptive transport and keep M2 mandatory reasoning floored ([#2928](https://github.com/can1357/oh-my-pi/issues/2928)).\n\n## [16.0.6] - 2026-06-18\n\n### Added\n\n- Added a dedicated `openrouter` API type and `ResolvedOpenRouterCompat` configuration to support unified chat-completions and Responses-API compatibility for OpenRouter models\n\n### Changed\n\n- Migrated bundled OpenRouter models in the catalog from `openai-completions` to the new `openrouter` API type\n- Consolidated the resolved OpenAI compat shape: extracted a shared `ResolvedOpenAISharedCompat` core that both `ResolvedOpenAICompat` and `ResolvedOpenAIResponsesCompat` extend (each builder still computes its own per-surface value, preserving chat↔Responses divergence), added internal resolved wire-quirk fields (`wireModelIdMode`, `stripDeepseekSpecialTokens`, `reasoningDeltasMayBeCumulative`, `emptyLengthFinishIsContextError`, `usesOpenAIToolCallIdLimit`, `dropThinkingWhenReasoningEffort`, `supportsObfuscationOptOut`), and replaced `buildOpenRouterCompat`'s cast-and-copy with an exhaustive `pickResponsesOnly` composition that fails to compile if a new Responses-only field is added without handling. The public `OpenAICompat` config vocabulary is unchanged.\n- Expanded `OpenAICompat`/`ResolvedOpenAISharedCompat` with shared reasoning/history/stream/request flags (`reasoningDisableMode`, `omitReasoningEffort`, `includeEncryptedReasoning`, `filterReasoningHistory`, `requiresReasoningContentForAllAssistantTurns`, `streamMarkupHealingPattern`, `promptCacheSessionHeader`, etc.) so model/provider/gateway constraints are declared once in catalog compat and then consumed uniformly by Chat Completions and Responses endpoints.\n\n### Fixed\n\n- Changed the default compatibility builder for `openai-completions` to set `requiresAssistantAfterToolResult` to `isMistral`, enabling the synthetic assistant bridge for built-in Mistral and Devstral models.\n- Fixed local Ollama (`provider: \"ollama\"`) reasoning turns still failing with HTTP 400 `invalid reasoning value: \"minimal\"` when the model was selected from a stale `~/.omp/models.db` cache row or a hand-written config: the `minimal → low` / `xhigh → max` remap was only stamped during fresh discovery, so cached and custom specs reached the wire unmapped. The remap now lives in the OpenAI chat-completions and Responses compat builders, so every `buildModel` (including cache loads, custom specs, and the `whenThinking` variant) backfills it — no `omp models refresh` required. Custom OpenAI-compatible providers registered under a non-`ollama` provider id still need their own `compat.reasoningEffortMap`.\n- Advertised Ollama Cloud GLM-5.2 reasoning efforts as high/xhigh-only and mapped `xhigh` to native max effort ([#2911](https://github.com/can1357/oh-my-pi/pull/2911) by [@serverinspector](https://github.com/serverinspector))\n- Fixed OpenRouter pseudo-API model construction so bundled OpenRouter models resolve shared OpenAI compatibility metadata instead of an undefined compat record.\n- Fixed custom/direct `xai-oauth` Responses model specs (e.g. `grok-build`) emitting `reasoning.effort` and hitting xAI's HTTP 400: `buildOpenAIResponsesCompat` now defaults `supportsReasoningEffort` to `false` for `xai-oauth` Grok models that are off the effort-capable allowlist (`grok-3-mini`/`grok-4.20-multi-agent`/`grok-4.3`), matching the curated discovery path; explicit `compat.supportsReasoningEffort` still overrides. The allowlist moved to a shared `isGrokReasoningEffortCapable` identity helper consumed by both the compat builder and provider-model curation so the two cannot drift.\n\n## [16.0.5] - 2026-06-17\n\n### Added\n\n- Added `enableGeminiThinkingLoopGuard` to OpenAI compatibility options to allow explicit opt-in or opt-out of the Gemini thinking-loop guard for OpenAI-compatible model aliases\n- Added `LITELLM_BASE_URL` as the LiteLLM provider discovery base URL fallback, with discovery caches scoped by the resolved proxy URL and explicit provider `baseUrl` config kept at higher precedence. ([#2726](https://github.com/can1357/oh-my-pi/issues/2726))\n- Added `ThinkingConfig.effortBudgets` (per-effort thinking-budget contract baked into collapsed variants) and `ANTIGRAVITY_MODEL_WIRE_PROFILES` (`maxOutputTokens` + `model_enum` per Antigravity wire id) to mirror the captured Antigravity Cloud Code Assist client request shape.\n\n### Changed\n\n- Defaulted `enableGeminiThinkingLoopGuard` from Gemini family detection for both OpenAI completions and responses compatibility specs so Gemini models now enable the thinking-loop guard automatically\n- Updated the default Gemini CLI user-agent version fallback to 0.46.0.\n- Changed the Antigravity (`google-antigravity`, daily-cloudcode-pa) gemini-3.x collapse families to the `budget` thinking transport with the client's per-tier `thinkingBudget` (3.5 Flash low/medium/high = 1000/4000/10000, 3.1 Pro low/high = 1001/10001) and corrected 3.5 Flash effort→wire routing (medium → `gemini-3.5-flash-low`, high → `gemini-3-flash-agent`). Split the shared CCA collapse table so `google-gemini-cli` (cloudcode-pa) keeps the `google-level` `thinkingLevel` transport for official Gemini CLI parity. Stale collapsed snapshots (bundled catalog, recycled `gemini-3-flash` alias) self-heal from the hand table at collapse time, and the model cache schema is bumped to v7 to invalidate pre-budget Antigravity rows.\n- Changed the Antigravity user-agent to the `antigravity/hub/<version>` format (default `2.1.4`) to match the captured client.\n\n### Fixed\n\n- Fixed `off` effort routing for `claude-opus-4-5` and `claude-opus-4-6` to use their base model IDs when thinking is disabled\n- Fixed `gemini-2.5-flash` effort routing so all non-off effort levels resolve to `gemini-2.5-flash-thinking`\n- Fixed shared variant alias provider resolution so `resolveBareVariantAlias` reports all matching providers when model aliases are present in both CCA collapse tables\n- Routed google-antigravity default baseUrl to the stable primary daily endpoint in the catalog generator and all fallback snapshots, resolving connection drops on heavy queries.\n- Fixed MiniMax M3 dialect selection so MiniMax-family OpenAI-compatible models use the MiniMax tool-call dialect instead of generic XML. ([#2759](https://github.com/can1357/oh-my-pi/issues/2759))\n- Fixed GitHub Copilot dynamic discovery to honor plan-specific API endpoints stored in structured OAuth credentials. ([#2876](https://github.com/can1357/oh-my-pi/issues/2876))\n\n## [16.0.4] - 2026-06-17\n\n### Fixed\n\n- Fixed GLM-5.2 catalog thinking metadata for Zhipu/BigModel so the top effort is exposed as `xhigh` and maps to provider-native `max`. ([#2833](https://github.com/can1357/oh-my-pi/issues/2833))\n\n## [16.0.2] - 2026-06-16\n\n### Fixed\n\n- Fixed Kimi output caps for Umans AI Coding Plan and Venice so discovery metadata cannot use context-sized token ceilings as request caps.\n- Marked Umans Anthropic-compatible models as client-tool escaped so cached and bundled metadata do not expose `web_search` as a provider server tool.\n\n## [16.0.1] - 2026-06-15\n\n### Added\n\n- Added the Umans AI Coding Plan provider catalog with Anthropic-compatible model metadata and dynamic discovery ([#2636](https://github.com/can1357/oh-my-pi/pull/2636) by [@oldschoola](https://github.com/oldschoola)).\n\n## [16.0.0] - 2026-06-15\n\n### Breaking Changes\n\n- Renamed the catalog-owned tool syntax API from `ToolCallSyntax`/`FALLBACK_TOOL_SYNTAX`/`preferredToolSyntax` to `Dialect`/`FALLBACK_DIALECT`/`preferredDialect`.\n\n## [15.13.3] - 2026-06-15\n\n### Added\n\n- Added Azure OpenAI as a catalog provider (`azure`, default model `gpt-5.5`, env var `AZURE_OPENAI_API_KEY`), bundling the OpenAI-family models Azure serves over the Responses API (GPT-4/4.1/4o, GPT-5 family, o-series, Codex). Like Amazon Bedrock it is catalog-only — models ship in the bundle and become selectable once the env key is set, with the deployment base URL resolved at runtime from `AZURE_OPENAI_BASE_URL`/`AZURE_OPENAI_RESOURCE_NAME`.\n- Added models.dev-backed bundled catalogs for providers that previously shipped no offline models: Hugging Face, Kilo, Moonshot, NanoGPT, Synthetic, Venice, Ollama Cloud, and the Xiaomi Token Plan regions (ams/cn/sgp). They still discover live when credentialed; the bundle is now a non-empty baseline.\n\n### Changed\n\n- Updated stale provider default models to their latest bundled versions: OpenAI-family providers (`azure`, `github-copilot`, `aimlapi`) → GPT-5.5; Gemini providers (`google`, `google-gemini-cli`, `google-vertex`) → `gemini-3.1-pro-preview`; GLM providers (`zai`, `zhipu-coding-plan`) → `glm-5.2`, `cerebras` → `zai-glm-4.7`; Kimi providers (`fireworks`, `opencode-go`, `moonshot`) → `kimi-k2.7-code`, `kimi-code` → `kimi-for-coding`, `together` → `moonshotai/Kimi-K2.7-Code`; `alibaba-coding-plan` → `qwen3.7-plus`; and Claude-Sonnet defaults (`cloudflare-ai-gateway`, `cursor`, `gitlab-duo`, `kilo`, `opencode-zen`, `vercel-ai-gateway`) → Claude Opus 4.x.\n- Restricted models.dev Azure discovery to OpenAI-family IDs (`gpt-`, `o1`, `o3`, `o4`, `codex`, `chatgpt`), excluding Foundry-hosted third parties (Claude/DeepSeek/Llama/Mistral/Phi) that Azure serves through non-Responses APIs.\n- Detected the Azure OpenAI Responses compat surface (developer role, strict tool mode, strict tool-result pairing) by provider id as well as base URL, so bundled `azure` models whose deployment host is only known at runtime still get the right wire behavior.\n- Renamed the `Qwen3-ASR-Flash` model label to `Qwen3 ASR Flash`\n\n### Fixed\n\n- Fixed tool syntax selection for Gemini-family and Gemma model IDs by routing them to dedicated `gemini` and `gemma` formats instead of generic XML\n- Fixed `zhipu-coding-plan` and `together` shipping no bundled models: their descriptors referenced non-existent models.dev keys (`zhipu-coding-plan`, `together`); pointed them at the real keys (`zhipuai-coding-plan`, `togetherai`) so they bundle their GLM and full catalogs respectively.\n- Folded the `azure-openai-responses` API into the OpenAI Responses thinking-inference branches so Azure reasoning models (o-series, GPT-5, Codex) resolve the discrete effort vocabulary (including `xhigh`) and effort-control mode instead of falling through to generic defaults.\n- Fixed `ollama-cloud` discovery inheriting an unsafe cross-provider `contextWindow`/`maxTokens` when `/api/show` returns no size metadata; it now falls back to the safe 128K context / 8K output caps.\n- Dropped internal Fireworks control-plane resource ids (`accounts/fireworks/{models,routers}/…`) from the bundle; only the public request ids ship.\n\n## [15.13.2] - 2026-06-15\n\n### Added\n\n- Added the `ToolCallSyntax` union and `FALLBACK_TOOL_SYNTAX` constant to `@oh-my-pi/pi-catalog/identity` (re-exported from `@oh-my-pi/pi-ai/grammar`).\n- Added `preferredToolSyntax(modelId)` to `@oh-my-pi/pi-catalog/identity`, resolving a model's native tool-call syntax affinity from its family token (Claude→`anthropic`, GLM→`glm`, Kimi→`kimi`, Qwen→`qwen3`, DeepSeek→`deepseek`, OpenAI/gpt-oss→`harmony`, else the `xml` fallback).\n- Added `flux-1-schnell-fp8` to the Fireworks serverless model catalog\n- Added `gpt-oss-20b` to the Fireworks model catalog\n- Added `qwen3-embedding-8b` to the Fireworks model catalog\n- Added `qwen3-reranker-8b` to the Fireworks model catalog\n- Added `Gemma 4 E2B IT` and `Gemma 4 E4B IT` to the Google model catalog\n- Added `qwen/qwen3-asr-flash` to the Zenmux model catalog\n- Added sparse `supportsTools` model metadata so providers can mark models that require in-band tool-call formatting.\n\n### Changed\n\n- Kept non-tool-capable Fireworks serverless models in discovery results and marked them with `supportsTools: false` for fallback-aware handling\n- Extended `modelFamilyToken(modelId)` to classify Claude/OpenAI ids the structured parser misses (older dated forms such as `claude-3-5-sonnet-20241022` and `gpt-4o`), returning `anthropic`/`openai` instead of an empty token.\n\n## [15.13.1] - 2026-06-15\n\n### Added\n\n- Added `modelFamilyToken(modelId)` to `@oh-my-pi/pi-catalog/identity`: a coarse vendor-lineage token (`anthropic`/`openai`/`gemini`/`kimi`/…) for \"are two models the same family?\" comparisons, backed by `parseKnownModel` canonical-id normalization. Opaque and comparison-only; kind/variant collapsed onto the vendor token ([#2406](https://github.com/can1357/oh-my-pi/issues/2406))\n\n### Changed\n\n- Changed catalog metadata to update a model’s per-token pricing to input 0.09 and output 0.18\n- Changed the same cataloged model’s maximum token limit from 384000 to 65536\n\n### Fixed\n\n- Fixed MiniMax-M3 catalog context for `minimax` and `minimax-cn` to report the documented 1M long-context tier instead of the upstream 512K pricing boundary ([#2576](https://github.com/can1357/oh-my-pi/issues/2576)).\n- Fixed OpenCode Go MiMo catalog metadata so title generation and other tool-enabled calls omit unsupported `tool_choice` instead of triggering provider 400s ([#2509](https://github.com/can1357/oh-my-pi/issues/2509)).\n- Fixed OpenCode Go `kimi-k2.7-code` catalog metadata so resolve-gate requests use automatic tool selection instead of Moonshot-rejected forced `tool_choice` ([#2546](https://github.com/can1357/oh-my-pi/issues/2546)).\n- Fixed Anthropic compat for the `github-copilot` host so `supportsEagerToolInputStreaming` defaults to `false` there, matching the Copilot proxy which rejects the per-tool `eager_input_streaming` field ([#2558](https://github.com/can1357/oh-my-pi/issues/2558)).\n- Scoped vLLM model cache validity to the discovery base URL so changed endpoints refetch immediately, and bounded built-in vLLM discovery requests with a timeout.\n\n## [15.12.6] - 2026-06-14\n\n### Added\n\n- Added GLM-5.2 to the bundled zai (GLM Coding Plan) catalog as the selectable 1M served model.\n\n### Changed\n\n- Pinned zai `glm-5.2` to 1M context during catalog generation so endpoint discovery and older fallbacks cannot regress it to 200k.\n- Replaced the hand-maintained `zhipu-coding-plan` GLM reasoning allowlist and vision regex with a `parseGlmModel` family classifier in `identity/classify.ts` (variant + vision + version), surfaced as `isReasoningGlmModelId` / `isGlmVisionModelId`. Discovery now derives reasoning/vision capability from the GLM family instead of a per-id list, so newly-bumped integers (`glm-5.3`, `glm-6`, …) are covered automatically while `-flash`/`-preview` and the vision `…v` shape stay correctly classified.\n\n## [15.12.4] - 2026-06-13\n\n### Added\n\n- Added bundled Fireworks models `deepseek-v4-flash`, `kimi-k2.7-code`, `minimax-m2.5`, `minimax-m3`, `nemotron-3-ultra-nvfp4`, `qwen3.6-plus`, and `qwen3.7-plus`\n- Changed\n\n### Changed\n\n- Model `contextWindow`/`maxTokens` are now `number | null`; discovery emits `null` when a provider reports no limit, replacing the `222222`/`8888` (`UNK_CONTEXT_WINDOW`/`UNK_MAX_TOKENS`) sentinels (now removed). Bundled `models.json` unknown limits are `null`.\n- Changed the `github-copilot` model context window to `524288` tokens\n- Changed Fireworks model discovery to source the control-plane `List Models` API (`GET /v1/accounts/fireworks/models?filter=supports_serverless=true`) instead of the OpenAI-compatible `/v1/models` inference listing. The inference endpoint returns a sparse, account-specific subset that omits on-demand serverless models (e.g. `kimi-k2.7-code`), so newly published serverless models stayed invisible in the picker until hand-added to the bundled catalog. The control-plane catalog enumerates every serverless model with capability metadata (`supportsServerless`/`supportsTools`/`supportsImageInput`/`contextLength`/`displayName`), paginated and filtered to tool-capable `READY` entries, then merged with bundled/models.dev references — the Kimi K2 max-output clamp and DeepSeek V4 thinking-toggle strip are preserved, and unbundled models default to reasoning so `buildModel` derives the Fireworks effort map. New serverless releases now surface automatically with no catalog edits.\n\n### Fixed\n\n- Filled missing `contextWindow` and `maxTokens` in generated `models.json` for proxy/reseller variants by inheriting limits from canonical-family and segment-reference models\n- Ignored zero-cost `x-ai` subscription entries as reference sources when backfilling limits so inflated values are not propagated\n- Fixed the model cache opening with `PRAGMA journal_mode=WAL` before `PRAGMA busy_timeout`, so concurrent omp startups could crash inside `getDb()` on `SQLITE_BUSY` during WAL recovery instead of waiting through the transient lock. The busy handler is now installed before the first lock-taking statement ([#2421](https://github.com/can1357/oh-my-pi/issues/2421)).\n\n## [15.11.8] - 2026-06-12\n\n### Fixed\n\n- Fixed Antigravity `gemini-3.1-pro --thinking high` failing with `Cloud Code Assist API error (400): Request contains an invalid argument.` — the upstream `gemini-3.1-pro-high` deployment rejects every `streamGenerateContent` request on both CCA endpoints while discovery still advertises it. High effort now routes to `gemini-pro-agent` (the same \"Gemini 3.1 Pro (High)\" model, verified accepting the identical request body), and the model-cache fingerprint version was bumped (`merge-v2` → `merge-v3`) so existing fresh caches refetch discovery and pick up the corrected routing immediately.\n\n## [15.11.7] - 2026-06-12\n\n### Added\n\n- Added effort-tier variant collapsing (`variant-collapse`): providers that expose one logical model as several effort/thinking-suffixed upstream ids (Antigravity CCA `gemini-3.5-flash-extra-low`/`-low`/`gemini-3-flash-agent`, `gemini-3[.1]-pro-low|high`, `claude-*[-thinking]` pairs, `gpt-oss-120b-medium`) collapse into one logical entry carrying per-effort upstream routing in `thinking.effortRouting` (plus `thinking.suppressWhenOff` for Cloud Code Assist ids whose baked server default re-applies when `thinkingConfig` is omitted). Request-time code resolves the outbound id via `resolveWireModelId(model, effort)`; selection, caching, and usage attribution key on the logical id.\n- Added the automatic `X`/`X-thinking` pair rule (`deriveThinkingPairFamilies`): any provider's live bare/thinking twin collapses into the bare id, routing thinking-enabled requests to the `-thinking` backing id (trailing or infix token, so `kimi-k2-thinking-turbo` pairs with `kimi-k2-turbo`). Gated on same api and compatible pricing — all-zero cost rows count as unknown, while twins that both carry real, differing prices remain separate SKUs.\n- Added `collapseBuiltModelVariants` and wired collapsing at every materialization point — Antigravity discovery, the catalog generator, and the model-manager merge — so stale sources (old static beside collapsed dynamic results, mixed cache rows) converge on logical entries instead of unioning raw tier ids back into the catalog.\n- Added `thinking.requiresEffort`, baked for reasoning-only upstreams — Gemini 3.x (levels only, no off), Gemini 2.5 Pro (thinkingBudget floors at 128, rejects 0), OpenAI o-series, MiniMax M2, and thinking-variant SKUs (`*-thinking`/`*-reasoner`/`*-reasoning`, with a negation-aware token grammar so `non-thinking` ids never match). Identity derivation bakes it for new entries and `fillThinkingWireDefaults` backfills explicit/cached metadata; `minimumSupportedEffort` exposes the canonical floor. Pair-collapsed twins drop member flags (their off routes to the bare SKU), while identity re-flags pairs whose logical id is itself mandatory\n\n### Changed\n\n- Changed model display names to drop model-extrinsic decorations: gateway author prefixes (`OpenAI: …`, `Google: …`), `(latest)` alias markers, `(Antigravity)` provider attribution, price tiers (`($$$$)`), and promo/lifecycle tags (`(20% off)`, `(retires …)`). `cleanModelName` is applied in `buildModel` (covers live discovery and stale caches) and as a catalog-generator pass; Antigravity discovery no longer appends `(Antigravity)` to display names. Variant tags that map to distinct wire ids (`(Thinking)`, `(free)`, `(Fast)`, dates, regions) are preserved.\n- Changed the `google-antigravity` default model from `gemini-3-pro-high` to `gemini-3.1-pro`\n- Changed `gemini-2.5-flash-thinking` handling from discovery-denylist to collapsing into `gemini-2.5-flash` (thinking-enabled requests route to the `-thinking` backing id)\n- Bumped the model cache schema to v5 so rows predating effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids) are invalidated\n\n### Fixed\n\n- Fixed catalog generation to apply effort-tier variant collapsing before provider grouping to ensure collapsed model families are consistently materialized without being impacted by in-loop mutation\n- Fixed Kimi K2.6 OpenAI-compatible compat metadata to use a 300s stream watchdog floor, covering Fire Pass router ids as well as public `kimi-k2.6` ids so long reasoning starts do not hit the generic first-event timeout ([#2366](https://github.com/can1357/oh-my-pi/issues/2366)).\n\n## [15.11.4] - 2026-06-12\n\n### Fixed\n\n- Fixed MiniMax M2-family and OpenAI gpt-oss model metadata so OpenAI-compatible catalog entries declare only `low|medium|high` thinking efforts. Their upstreams reject `minimal`, `xhigh`, and Fireworks' `minimal → none` wire mapping, so `fireworks/minimax-m2.7` as the smol auto-thinking classifier model 400ed on every turn. OpenAI-compatible provider effort maps (`Groq qwen/qwen3-32b`, DeepSeek-family, OpenRouter Anthropic adaptive, Fireworks `minimal → none`) now bake into `thinking.effortMap` in catalog metadata instead of `buildOpenAICompat`, and request builders read that field directly. Regenerated `models.json` now makes `disableReasoning` choose `low` for those families while leaving GLM-5.x and other Fireworks models on the existing `minimal → none` path ([#2315](https://github.com/can1357/oh-my-pi/issues/2315)).\n\n### Added\n\n- Added `requiresJuiceZeroHack` Responses-API compat flag, resolved by `buildOpenAIResponsesCompat` from GPT-5-family model names and overridable via sparse model `compat` config. Replaces the request-time `model.name.startsWith(\"gpt-5\")` sniff that gated the trailing `# Juice: 0 !important` no-reasoning developer item.\n\n## [15.11.3] - 2026-06-11\n\n### Added\n\n- Added `requestModelId` on `Model` to represent the upstream model id used when a catalog entry is a local variant\n- Added synthetic GitHub Copilot long-context model variants with `-1m` suffixes when tiered token pricing is advertised\n\n### Changed\n\n- Changed GitHub Copilot discovery to request `X-GitHub-Api-Version: 2026-06-01` from `api.githubcopilot.com`\n- Changed GitHub Copilot discovery to cap base model `contextWindow` to the default token tier and keep long-context access as the separate `-1m` model entry\n- Changed Copilot model mapping to omit non-chat `/models` entries and enable image input for models whose capabilities indicate vision support\n\n### Fixed\n\n- Fixed long-context variant pricing to use `billing.token_prices.long_context` rates instead of default model pricing\n- Fixed `mapModel` handling in OpenAI-compatible discovery so returning `null` now skips a model entry rather than falling back to defaults\n- Fixed model ID precedence so a real upstream Copilot model id is kept when it conflicts with a synthesized `-1m` variant\n\n## [15.11.1] - 2026-06-11\n\n### Fixed\n\n- Fixed NVIDIA NIM Qwen turns failing with `400 Validation: Unsupported parameter(s): enable_thinking`. NIM's chat-completions schema is `additionalProperties: false` and exposes thinking via the vLLM convention `chat_template_kwargs.enable_thinking`; `buildOpenAICompat` was sending top-level `enable_thinking` for every `qwen/*` id regardless of host. Registered `nvidia` as a known host (`integrate.api.nvidia.com`) and routed NVIDIA-hosted Qwen models to `thinkingFormat: \"qwen-chat-template\"` ([#2299](https://github.com/can1357/oh-my-pi/issues/2299)).\n- Fixed Moonshot/Kimi native OpenAI-compatible request metadata so Kimi K2 uses `max_tokens` and omits OpenAI-only `store`, restoring first-turn output with `MOONSHOT_API_KEY` ([#2289](https://github.com/can1357/oh-my-pi/issues/2289)).\n\n## [15.11.0] - 2026-06-10\n\n### Fixed\n\n- Fixed `buildModel` so malformed explicit thinking metadata without `efforts` is treated as sparse input and inferred instead of crashing during model resolution ([#2251](https://github.com/can1357/oh-my-pi/issues/2251)).\n\n## [15.10.12] - 2026-06-10\n\n### Added\n\n- Added `grok-composer-2.5-fast` (Cursor \"Composer 2.5 Fast\") to the xAI Grok OAuth (SuperGrok) catalog: non-reasoning, text-only, 200K context.\n\n### Changed\n\n- Set every xAI Grok OAuth (SuperGrok) curated model's max output tokens to mirror its context window (`grok-build`, `grok-4.3`, `grok-4.20-0309-{reasoning,non-reasoning}`, `grok-4.20-multi-agent-0309`, `grok-composer-2.5-fast`), replacing the `8888` `UNK_MAX_TOKENS` placeholder (and a stale `30000` on three grok-4.x entries). xAI's OAuth `/v1/models` reports no per-request output limit, so the curated catalog now owns `maxTokens` like `contextWindow`, deterministic on both the static-seed and online-overlay paths; the `openai-responses` wire still clamps the actual request to `OPENAI_MAX_OUTPUT_TOKENS` (64k).\n\n### Fixed\n\n- Excluded zero-cost `xai-oauth` subscription entries from the model reference indexes (`buildModelReferenceIndex`, `createReferenceResolver`), so their zero pricing and context-window-sized `maxTokens` cannot outrank paid/public Grok references when resolving custom-provider model identities.\n\n## [15.10.11] - 2026-06-10\n\n### Added\n\n- Added `hostMatchesUrl`, `modelMatchesHost`, and endpoint-shape helpers in the new `hosts` module for consistent provider/baseUrl matching\n- `buildModel(spec)` (`build.ts`) is now the single Model constructor: it materializes the fully-resolved compat record and canonical thinking metadata exactly once (compat first, thinking derived from identity + resolved compat), so `Model.compat` is a required, complete `CompatOf<TApi>` (`ResolvedOpenAICompat`/`ResolvedOpenAIResponsesCompat`/`ResolvedAnthropicCompat`) and request-path code reads fields with zero URL parsing and zero per-request allocation. Sparse user/config overrides live on the new `ModelSpec<TApi>` input shape and survive on `Model.compatConfig` for introspection.\n- Added `ResolvedAnthropicCompat.supportsSamplingParams` (Opus 4.7+/Fable/Mythos reject `temperature`/`top_p`/`top_k` with a 400), baked at build time from model identity so the request path stops re-parsing model ids.\n- Compat detection gained model-time flags so handlers stop sniffing baseUrl: completions `supportsReasoningParams`, `alwaysSendMaxTokens`, `isOpenRouterHost`, `isVercelGatewayHost`, `streamIdleTimeoutMs`, and a precomputed `whenThinking` alternate view (OpenCode `reasoning_content` gating, #1071/#1484); responses `strictResponsesPairing`, `supportsLongPromptCacheRetention`, `supportsReasoningEffort`; anthropic `officialEndpoint`, `requiresToolResultId`, `replayUnsignedThinking`.\n- New `@oh-my-pi/pi-catalog` package: the model catalog extracted from `@oh-my-pi/pi-ai`. Owns the bundled `models.json` and its generation pipeline (`scripts/generate-models.ts`), the core model data types (`Model`, `Api`, `ThinkingConfig`, `Effort`, `Usage`, compat interfaces), thinking metadata enrichment and generated policies (`model-thinking.ts`), the SQLite model cache and model manager, per-provider discovery factories (`provider-models/`), the discovery protocol clients (`discovery/`), and the new `CATALOG_PROVIDERS` table — the single source of truth for provider ids, default models, and discovery wiring (`KnownProvider`, `PROVIDER_DESCRIPTORS`, and `DEFAULT_MODEL_PER_PROVIDER` are derived from it).\n- New `identity/` module centralizing model-identity concerns that were previously duplicated across packages: family classification and version parsing (`identity/classify.ts`, extracted from pi-ai's `model-thinking` internals), canonical model equivalence with injected reference data (`identity/equivalence.ts`, from coding-agent's `model-equivalence`), proxy/reseller reference lookup (`identity/reference.ts`, from coding-agent's `model-registry`), bracket-affix and id-segment helpers (`identity/id.ts`), a single trailing-marker vocabulary with canonical vs reference flavors (`identity/markers.ts` — `search` stays reference-only so Perplexity's `sonar-pro-search` remains canonical-distinct), and provider priority ordering (`identity/priority.ts`).\n- Memoized bundled-reference accessors (`getBundledCanonicalReferenceData` / `getBundledModelReferenceIndex` in `identity/bundled.ts`): one lazy walk of the bundled catalog feeds both canonical equivalence and proxy-reference lookup, so consumers no longer hand-roll the glue.\n- `identity/selection.ts`: pure canonical-variant selection (`resolveCanonicalVariant`, `buildCanonicalModelOrder`, `CanonicalVariantPreferences`) extracted from the coding-agent registry — provider rank, then exact-id match, variant source, id length, and candidate order.\n\n### Changed\n\n- Changed OpenAI compatibility detection to use shared host classifiers (`modelMatchesHost`/`hostMatchesUrl`) with normalized matching instead of raw URL substring checks\n- Changed `hostMatchesUrl`/`modelMatchesHost` usage in compatibility detection to reduce mismatches across case variants and provider alias hosts\n- Provider catalog entries now carry the runtime API-key env fallback as an ordered `envVars` list; `catalogDiscovery.envVars` became an optional generation-time override (only `cursor` and `vercel-ai-gateway` differ) and `PROVIDER_DESCRIPTORS` materializes the resolved list for `generate-models.ts`.\n- `Model`'s api parameter now defaults to `Api` instead of `any` (`Model<TApi extends Api = Api>`), so bare `Model` no longer behaves as `Model<any>` at call sites.\n- `ThinkingConfig` is now explicit and total: an ordered `efforts` array replaces the `minLevel`/`maxLevel`/`levels` range encoding, and the wire facts are baked alongside it — `effortMap` (anthropic-adaptive 4-tier vs 5-tier scale, shared with the OpenRouter completions remap) and `supportsDisplay` (adaptive `display` field support). Explicit spec thinking owns the capability surface (`mode`/`efforts`/`defaultLevel`) and wins over inference; missing wire facts are backfilled from identity so configs never need to know Anthropic's tier tables. Reasoning models that reject the wire effort param (`compat.supportsReasoningEffort: false` on openai-responses*) are encoded as `thinking: undefined` (\"thinks, no control surface\") instead of the removed `modelOmitsReasoningEffort` special case. `models.json` was re-baked in the new vocabulary behind a 3196-model behavioral parity gate, and the model cache schema bumped to v4 to invalidate old-shape rows.\n- `mapEffortToGoogleThinkingLevel(effort)` is now a static map (model parameter dropped — validation stays at the `requireSupportedEffort` call sites), and `mapEffortToAnthropicAdaptiveEffort` reads the baked `thinking.effortMap` instead of re-classifying the model id per request.\n- Generator-only policy code moved out of the runtime bundle into `scripts/generated-policies.ts`: `applyGeneratedModelPolicies` (now policy fixups + thinking re-bake via the shared deriver), `linkOpenAIPromotionTargets`, the Copilot context-window table, minimax/opencode-go compat fixups, and `CLOUDFLARE_FALLBACK_MODEL`. The anthropic id predicates (`hasOpus47ApiRestrictions`, `supportsMidConversationSystemMessages`, `isAnthropicFableOrMythosModel`) moved to `identity/family` for build-time use by the compat/thinking derivers only.\n\n### Fixed\n\n- Fixed Anthropic official-endpoint detection to require strict HTTPS hostname matching so non-official or lookalike URLs are no longer treated as official Anthropic hosts\n- Fixed Ollama Cloud dynamic discovery so same-id matches from other providers no longer supply context-window or max-output-token limits for discovered models.\n- Wired `@oh-my-pi/pi-catalog` into the release publish package list, tarball install smoke test, and root `bun generate-models` script.\n- Fixed `supportsAdaptiveThinkingDisplay` only matching dash-form version ids: dotted ids (`claude-opus-4.7`) now classify through `identity/classify` like every other anthropic predicate, so six bundled dotted Opus 4.7/4.8 entries (github-copilot, vercel-ai-gateway, zenmux) regain adaptive `display` support; bare dated ids (`claude-opus-4-20250514` = Opus 4.0) stay excluded.\n- Fixed the OpenRouter anthropic adaptive-effort map misclassifying bare dated Opus ids (`claude-opus-4-20250514` parsed as version 4.20 → wrongly adaptive); the map now derives from the shared classifier and the shared 4-/5-tier tables.\n\n### Removed\n\n- Removed the runtime enrichment layer: `enrichModelThinking` (and its non-enumerable memo-slot cache), `refreshModelThinking`, `modelOmitsReasoningEffort`, and the `model-thinking` re-exports of generator-only policies. Thinking metadata is resolved exactly once inside `buildModel`; runtime helpers (`getSupportedEfforts`, `clampThinkingLevelForModel`, `requireSupportedEffort`, the effort mappers) are pure field reads.\n"}}, "packages/ai/src/registry/alibaba-coding-plan.ts": {"06343fef4200c4e32d18f08df5a6a8bd84dcc710": {"t": 1785674196.5994406, "n": 3240, "h": "96217a2c32fc1cad", "c": "import * as AIError from \"../error\";\nimport * as apiKeyValidation from \"./api-key-validation\";\nimport type { OAuthController, OAuthCredentials, OAuthLoginCallbacks } from \"./oauth/types\";\nimport type { ProviderDefinition } from \"./types\";\n\nconst DEFAULT_AUTH_URL = \"https://modelstudio.console.alibabacloud.com/\";\nconst CHINA_AUTH_URL = \"https://dashscope.console.aliyun.com/\";\nconst DEFAULT_API_BASE_URL = \"https://coding-intl.dashscope.aliyuncs.com/v1\";\nconst CHINA_API_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\";\nconst VALIDATION_MODEL = \"qwen3.5-plus\";\n\nexport async function loginAlibabaCodingPlan(options: OAuthController): Promise<OAuthCredentials> {\n\tif (!options.onPrompt) {\n\t\tthrow new AIError.OnPromptRequiredError(\"Alibaba Coding Plan\");\n\t}\n\n\t// Ask which endpoint to use\n\tconst endpointChoice = await options.onPrompt({\n\t\tmessage: \"Select Alibaba Coding Plan endpoint: 1=International (default), 2=China, 3=Custom — enter 1, 2, or 3\",\n\t\tplaceholder: \"1\",\n\t});\n\n\t// Check for abort after endpoint selection (Escape returns \"\")\n\tif (options.signal?.aborted) {\n\t\tthrow new AIError.LoginCancelledError();\n\t}\n\n\tconst choice = endpointChoice.trim();\n\tlet baseUrl: string;\n\tlet authUrl: string;\n\tlet instructions: string;\n\tif (choice === \"2\") {\n\t\tbaseUrl = CHINA_API_BASE_URL;\n\t\tauthUrl = CHINA_AUTH_URL;\n\t\tinstructions = \"Copy your API key from the Alibaba Cloud DashScope console (China mainland)\";\n\t} else if (choice === \"3\") {\n\t\tconst customUrl = await options.onPrompt({\n\t\t\tmessage: \"Enter custom base URL\",\n\t\t\tplaceholder: \"https://your-proxy.com/v1\",\n\t\t});\n\t\tconst trimmedUrl = customUrl.trim().replace(/\\/+$/, \"\");\n\t\tif (!trimmedUrl) {\n\t\t\tthrow new AIError.ConfigurationError(\"Custom URL is required for option 3\");\n\t\t}\n\t\tbaseUrl = trimmedUrl;\n\t\tauthUrl = DEFAULT_AUTH_URL;\n\t\tinstructions = \"Copy your API key from the Alibaba Cloud DashScope console\";\n\t} else {\n\t\tbaseUrl = DEFAULT_API_BASE_URL;\n\t\tauthUrl = DEFAULT_AUTH_URL;\n\t\tinstructions = \"Copy your API key from the Alibaba Cloud DashScope console (International)\";\n\t}\n\n\toptions.onAuth?.({\n\t\turl: authUrl,\n\t\tinstructions,\n\t});\n\n\tconst apiKey = await options.onPrompt({\n\t\tmessage: \"Paste your Alibaba Coding Plan API key\",\n\t\tplaceholder: \"sk-...\",\n\t});\n\n\tif (options.signal?.aborted) {\n\t\tthrow new AIError.LoginCancelledError();\n\t}\n\n\tconst trimmed = apiKey.trim();\n\tif (!trimmed) {\n\t\tthrow new AIError.ApiKeyRequiredError();\n\t}\n\n\toptions.onProgress?.(\"Validating API key...\");\n\tif (choice === \"3\") {\n\t\tawait apiKeyValidation.validateApiKeyAgainstModelsEndpoint({\n\t\t\tprovider: \"Alibaba Coding Plan\",\n\t\t\tapiKey: trimmed,\n\t\t\tmodelsUrl: `${baseUrl}/models`,\n\t\t\tsignal: options.signal,\n\t\t});\n\t} else {\n\t\tawait apiKeyValidation.validateOpenAICompatibleApiKey({\n\t\t\tprovider: \"Alibaba Coding Plan\",\n\t\t\tapiKey: trimmed,\n\t\t\tbaseUrl,\n\t\t\tmodel: VALIDATION_MODEL,\n\t\t\tsignal: options.signal,\n\t\t});\n\t}\n\n\treturn {\n\t\taccess: trimmed,\n\t\trefresh: trimmed,\n\t\texpires: Number.MAX_SAFE_INTEGER,\n\t\tenterpriseUrl: baseUrl,\n\t};\n}\n\nexport const alibabaCodingPlanProvider = {\n\tid: \"alibaba-coding-plan\",\n\tname: \"Alibaba Coding Plan\",\n\tlogin: (cb: OAuthLoginCallbacks) => loginAlibabaCodingPlan(cb),\n\tgetApiKey: credentials => credentials.access,\n} as const satisfies ProviderDefinition;\n"}, "003bb5548c7c1e759336e264305790de435708d1": {"t": 1785846846.704709, "n": 3240, "h": "96217a2c32fc1cad", "c": "import * as AIError from \"../error\";\nimport * as apiKeyValidation from \"./api-key-validation\";\nimport type { OAuthController, OAuthCredentials, OAuthLoginCallbacks } from \"./oauth/types\";\nimport type { ProviderDefinition } from \"./types\";\n\nconst DEFAULT_AUTH_URL = \"https://modelstudio.console.alibabacloud.com/\";\nconst CHINA_AUTH_URL = \"https://dashscope.console.aliyun.com/\";\nconst DEFAULT_API_BASE_URL = \"https://coding-intl.dashscope.aliyuncs.com/v1\";\nconst CHINA_API_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\";\nconst VALIDATION_MODEL = \"qwen3.5-plus\";\n\nexport async function loginAlibabaCodingPlan(options: OAuthController): Promise<OAuthCredentials> {\n\tif (!options.onPrompt) {\n\t\tthrow new AIError.OnPromptRequiredError(\"Alibaba Coding Plan\");\n\t}\n\n\t// Ask which endpoint to use\n\tconst endpointChoice = await options.onPrompt({\n\t\tmessage: \"Select Alibaba Coding Plan endpoint: 1=International (default), 2=China, 3=Custom — enter 1, 2, or 3\",\n\t\tplaceholder: \"1\",\n\t});\n\n\t// Check for abort after endpoint selection (Escape returns \"\")\n\tif (options.signal?.aborted) {\n\t\tthrow new AIError.LoginCancelledError();\n\t}\n\n\tconst choice = endpointChoice.trim();\n\tlet baseUrl: string;\n\tlet authUrl: string;\n\tlet instructions: string;\n\tif (choice === \"2\") {\n\t\tbaseUrl = CHINA_API_BASE_URL;\n\t\tauthUrl = CHINA_AUTH_URL;\n\t\tinstructions = \"Copy your API key from the Alibaba Cloud DashScope console (China mainland)\";\n\t} else if (choice === \"3\") {\n\t\tconst customUrl = await options.onPrompt({\n\t\t\tmessage: \"Enter custom base URL\",\n\t\t\tplaceholder: \"https://your-proxy.com/v1\",\n\t\t});\n\t\tconst trimmedUrl = customUrl.trim().replace(/\\/+$/, \"\");\n\t\tif (!trimmedUrl) {\n\t\t\tthrow new AIError.ConfigurationError(\"Custom URL is required for option 3\");\n\t\t}\n\t\tbaseUrl = trimmedUrl;\n\t\tauthUrl = DEFAULT_AUTH_URL;\n\t\tinstructions = \"Copy your API key from the Alibaba Cloud DashScope console\";\n\t} else {\n\t\tbaseUrl = DEFAULT_API_BASE_URL;\n\t\tauthUrl = DEFAULT_AUTH_URL;\n\t\tinstructions = \"Copy your API key from the Alibaba Cloud DashScope console (International)\";\n\t}\n\n\toptions.onAuth?.({\n\t\turl: authUrl,\n\t\tinstructions,\n\t});\n\n\tconst apiKey = await options.onPrompt({\n\t\tmessage: \"Paste your Alibaba Coding Plan API key\",\n\t\tplaceholder: \"sk-...\",\n\t});\n\n\tif (options.signal?.aborted) {\n\t\tthrow new AIError.LoginCancelledError();\n\t}\n\n\tconst trimmed = apiKey.trim();\n\tif (!trimmed) {\n\t\tthrow new AIError.ApiKeyRequiredError();\n\t}\n\n\toptions.onProgress?.(\"Validating API key...\");\n\tif (choice === \"3\") {\n\t\tawait apiKeyValidation.validateApiKeyAgainstModelsEndpoint({\n\t\t\tprovider: \"Alibaba Coding Plan\",\n\t\t\tapiKey: trimmed,\n\t\t\tmodelsUrl: `${baseUrl}/models`,\n\t\t\tsignal: options.signal,\n\t\t});\n\t} else {\n\t\tawait apiKeyValidation.validateOpenAICompatibleApiKey({\n\t\t\tprovider: \"Alibaba Coding Plan\",\n\t\t\tapiKey: trimmed,\n\t\t\tbaseUrl,\n\t\t\tmodel: VALIDATION_MODEL,\n\t\t\tsignal: options.signal,\n\t\t});\n\t}\n\n\treturn {\n\t\taccess: trimmed,\n\t\trefresh: trimmed,\n\t\texpires: Number.MAX_SAFE_INTEGER,\n\t\tenterpriseUrl: baseUrl,\n\t};\n}\n\nexport const alibabaCodingPlanProvider = {\n\tid: \"alibaba-coding-plan\",\n\tname: \"Alibaba Coding Plan\",\n\tlogin: (cb: OAuthLoginCallbacks) => loginAlibabaCodingPlan(cb),\n\tgetApiKey: credentials => credentials.access,\n} as const satisfies ProviderDefinition;\n"}}, "packages/ai/README.md": {"06343fef4200c4e32d18f08df5a6a8bd84dcc710": {"t": 1785674664.4766407, "n": 45340, "h": "8aec5ecad840718a", "c": "# @oh-my-pi/pi-ai\n\nUnified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session.\n\n**Note**: This library only includes models that support tool calling (function calling), as this is essential for agentic workflows.\n\n## Table of Contents\n\n- [Supported Providers](#supported-providers)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Tools](#tools)\n - [Defining Tools](#defining-tools)\n - [Handling Tool Calls](#handling-tool-calls)\n - [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)\n - [Validating Tool Arguments](#validating-tool-arguments)\n - [Complete Event Reference](#complete-event-reference)\n- [Image Input](#image-input)\n- [Thinking/Reasoning](#thinkingreasoning)\n - [Unified Interface](#unified-interface-streamsimplecompletesimple)\n - [Provider-Specific Options](#provider-specific-options-streamcomplete)\n - [Streaming Thinking Content](#streaming-thinking-content)\n- [Stop Reasons](#stop-reasons)\n- [Error Handling](#error-handling)\n - [Aborting Requests](#aborting-requests)\n - [Continuing After Abort](#continuing-after-abort)\n- [APIs, Models, and Providers](#apis-models-and-providers)\n - [Providers and Models](#providers-and-models)\n - [Querying Providers and Models](#querying-providers-and-models)\n - [Custom Models](#custom-models)\n - [OpenAI Compatibility Settings](#openai-compatibility-settings)\n - [Type Safety](#type-safety)\n- [Cross-Provider Handoffs](#cross-provider-handoffs)\n- [Context Serialization](#context-serialization)\n- [Browser Usage](#browser-usage)\n - [Environment Variables](#environment-variables-nodejs-only)\n - [Checking Environment Variables](#checking-environment-variables)\n- [OAuth Providers](#oauth-providers)\n - [Vertex AI (ADC)](#vertex-ai-adc)\n - [CLI Login](#cli-login)\n - [Programmatic OAuth](#programmatic-oauth)\n - [Login Flow Example](#login-flow-example)\n - [Using OAuth Tokens](#using-oauth-tokens)\n - [Provider Notes](#provider-notes)\n- [License](#license)\n\n## Supported Providers\n\n- **OpenAI**\n- **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)\n- **Anthropic**\n- **Google**\n- **Vertex AI** (Gemini via Vertex AI)\n- **Mistral**\n- **Groq**\n- **Cerebras**\n- **Together**\n- **Moonshot** (requires `MOONSHOT_API_KEY`)\n- **Qianfan** (requires `QIANFAN_API_KEY`)\n- **NVIDIA** (requires `NVIDIA_API_KEY`)\n- **NanoGPT** (requires `NANO_GPT_API_KEY`)\n- **Novita** (requires `NOVITA_API_KEY`)\n- **Hugging Face Inference**\n- **xAI**\n- **Venice** (requires `VENICE_API_KEY`)\n- **Wafer Serverless** (requires `WAFER_SERVERLESS_API_KEY`; pay-as-you-go)\n- **OpenRouter**\n- **Kilo Gateway** (supports OAuth `/login kilo` or `KILO_API_KEY`)\n- **LiteLLM** (requires `LITELLM_API_KEY`)\n- **zAI** (requires `ZAI_API_KEY`)\n- **Umans AI Coding Plan** (supports `/login umans` or `UMANS_AI_CODING_PLAN_API_KEY`)\n- **MiniMax Token Plan** (requires `MINIMAX_CODE_API_KEY` or `MINIMAX_CODE_CN_API_KEY`)\n- **Xiaomi MiMo** (requires `XIAOMI_API_KEY`)\n- **ZenMux** (requires `ZENMUX_API_KEY`)\n- **Qwen Portal** (supports `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY`)\n- **QwenCloud Token Plan** (supports `/login alibaba-token-plan`, `ALIBABA_TOKEN_PLAN_API_KEY`, or `BAILIAN_TOKEN_PLAN_API_KEY`; interactive login first selects a region — International (Singapore, default), China (Beijing) for 百炼 Token Plan keys, or a custom base URL — since region keys are non-interchangeable, then optionally stores a `home.qwencloud.com` Cookie request header for best-effort 5-hour and 7-day quota reporting)\n To enable quota reporting, sign in to the Token Plan dashboard, copy the `Cookie` request-header value from a `home.qwencloud.com` request in browser developer tools, and paste it at the second login prompt. Press Enter to skip; the Cookie is sensitive and session-lived, so rerun login when it expires.\n- **Cloudflare AI Gateway** (requires `CLOUDFLARE_AI_GATEWAY_API_KEY` and provider-specific gateway base URL)\n- **Ollama** (local OpenAI-compatible runtime; optional `OLLAMA_API_KEY`)\n- **Ollama Cloud** (hosted native Ollama API; requires `OLLAMA_CLOUD_API_KEY`)\n- **llama.cpp** (local OpenAI and Anthropic compatible inference server)\n- **vLLM** (OpenAI-compatible server; `VLLM_API_KEY` for secured deployments)\n- **GitHub Copilot** (requires OAuth, see below)\n- **Google Gemini CLI** (requires OAuth, see below)\n- **Antigravity** (requires OAuth, see below)\n- **Any OpenAI-compatible API**: LM Studio, custom proxies, etc.\n\n## Installation\n\n```bash\nnpm install @oh-my-pi/pi-ai\n```\n\n## Quick Start\n\n```typescript\nimport { z, getModel, stream, complete, Context, Tool } from \"@oh-my-pi/pi-ai\";\n\n// Fully typed with auto-complete support for both providers and models\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Define tools with Zod schemas for type safety and validation\nconst tools: Tool[] = [\n\t{\n\t\tname: \"get_time\",\n\t\tdescription: \"Get the current time\",\n\t\tparameters: z.object({\n\t\t\ttimezone: z\n\t\t\t\t.string()\n\t\t\t\t.optional()\n\t\t\t\t.describe(\"Optional timezone (e.g., America/New_York)\"),\n\t\t}),\n\t},\n];\n\n// Build a conversation context (easily serializable and transferable between models)\nconst context: Context = {\n\tsystemPrompt: [\"You are a helpful assistant.\"],\n\tmessages: [{ role: \"user\", content: \"What time is it?\" }],\n\ttools,\n};\n\n// Option 1: Streaming with all event types\nconst s = stream(model, context);\n\nfor await (const event of s) {\n\tswitch (event.type) {\n\t\tcase \"start\":\n\t\t\tconsole.log(`Starting with ${event.partial.model}`);\n\t\t\tbreak;\n\t\tcase \"text_start\":\n\t\t\tconsole.log(\"\\n[Text started]\");\n\t\t\tbreak;\n\t\tcase \"text_delta\":\n\t\t\tprocess.stdout.write(event.delta);\n\t\t\tbreak;\n\t\tcase \"text_end\":\n\t\t\tconsole.log(\"\\n[Text ended]\");\n\t\t\tbreak;\n\t\tcase \"thinking_start\":\n\t\t\tconsole.log(\"[Model is thinking...]\");\n\t\t\tbreak;\n\t\tcase \"thinking_delta\":\n\t\t\tprocess.stdout.write(event.delta);\n\t\t\tbreak;\n\t\tcase \"thinking_end\":\n\t\t\tconsole.log(\"[Thinking complete]\");\n\t\t\tbreak;\n\t\tcase \"toolcall_start\":\n\t\t\tconsole.log(`\\n[Tool call started: index ${event.contentIndex}]`);\n\t\t\tbreak;\n\t\tcase \"toolcall_delta\":\n\t\t\t// Partial tool arguments are being streamed\n\t\t\tconst partialCall = event.partial.content[event.contentIndex];\n\t\t\tif (partialCall.type === \"toolCall\") {\n\t\t\t\tconsole.log(`[Streaming args for ${partialCall.name}]`);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"toolcall_end\":\n\t\t\tconsole.log(`\\nTool called: ${event.toolCall.name}`);\n\t\t\tconsole.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`);\n\t\t\tbreak;\n\t\tcase \"done\":\n\t\t\tconsole.log(`\\nFinished: ${event.reason}`);\n\t\t\tbreak;\n\t\tcase \"error\":\n\t\t\tconsole.error(`Error: ${event.error}`);\n\t\t\tbreak;\n\t}\n}\n\n// Get the final message after streaming, add it to the context\nconst finalMessage = await s.result();\ncontext.messages.push(finalMessage);\n\n// Handle tool calls if any\nconst toolCalls = finalMessage.content.filter((b) => b.type === \"toolCall\");\nfor (const call of toolCalls) {\n\t// Execute the tool\n\tconst result =\n\t\tcall.name === \"get_time\"\n\t\t\t? new Date().toLocaleString(\"en-US\", {\n\t\t\t\t\ttimeZone: call.arguments.timezone || \"UTC\",\n\t\t\t\t\tdateStyle: \"full\",\n\t\t\t\t\ttimeStyle: \"long\",\n\t\t\t\t})\n\t\t\t: \"Unknown tool\";\n\n\t// Add tool result to context (supports text and images)\n\tcontext.messages.push({\n\t\trole: \"toolResult\",\n\t\ttoolCallId: call.id,\n\t\ttoolName: call.name,\n\t\tcontent: [{ type: \"text\", text: result }],\n\t\tisError: false,\n\t\ttimestamp: Date.now(),\n\t});\n}\n\n// Continue if there were tool calls\nif (toolCalls.length > 0) {\n\tconst continuation = await complete(model, context);\n\tcontext.messages.push(continuation);\n\tconsole.log(\"After tool execution:\", continuation.content);\n}\n\nconsole.log(`Total tokens: ${finalMessage.usage.input} in, ${finalMessage.usage.output} out`);\nconsole.log(`Cost: $${finalMessage.usage.cost.total.toFixed(4)}`);\n\n// Option 2: Get complete response without streaming\nconst response = await complete(model, context);\n\nfor (const block of response.content) {\n\tif (block.type === \"text\") {\n\t\tconsole.log(block.text);\n\t} else if (block.type === \"toolCall\") {\n\t\tconsole.log(`Tool: ${block.name}(${JSON.stringify(block.arguments)})`);\n\t}\n}\n```\n\n## Tools\n\nTools enable LLMs to interact with external systems. This library uses **Zod** schemas for type-safe tool definitions with automatic validation. Schemas are converted to JSON Schema for providers as needed.\n\n### Defining Tools\n\n```typescript\nimport { z, Tool } from \"@oh-my-pi/pi-ai\";\n\n// Define tool parameters with Zod\nconst weatherTool: Tool = {\n\tname: \"get_weather\",\n\tdescription: \"Get current weather for a location\",\n\tparameters: z.object({\n\t\tlocation: z.string().describe(\"City name or coordinates\"),\n\t\tunits: z.enum([\"celsius\", \"fahrenheit\"]).default(\"celsius\"),\n\t}),\n};\n\nconst bookMeetingTool: Tool = {\n\tname: \"book_meeting\",\n\tdescription: \"Schedule a meeting\",\n\tparameters: z.object({\n\t\ttitle: z.string().min(1),\n\t\tstartTime: z.string().describe(\"ISO 8601 date-time\"),\n\t\tendTime: z.string().describe(\"ISO 8601 date-time\"),\n\t\tattendees: z.array(z.email()).min(1),\n\t}),\n};\n```\n\n### Handling Tool Calls\n\nTool results use content blocks and can include both text and images:\n\n```typescript\nimport * as fs from \"node:fs\";\n\nconst context: Context = {\n\tmessages: [{ role: \"user\", content: \"What is the weather in London?\" }],\n\ttools: [weatherTool],\n};\n\nconst response = await complete(model, context);\n\n// Check for tool calls in the response\nfor (const block of response.content) {\n\tif (block.type === \"toolCall\") {\n\t\t// Execute your tool with the arguments\n\t\t// See \"Validating Tool Arguments\" section for validation\n\t\tconst result = await executeWeatherApi(block.arguments);\n\n\t\t// Add tool result with text content\n\t\tcontext.messages.push({\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId: block.id,\n\t\t\ttoolName: block.name,\n\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result) }],\n\t\t\tisError: false,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n}\n\n// Tool results can also include images (for vision-capable models)\nconst imageBuffer = fs.readFileSync(\"chart.png\");\ncontext.messages.push({\n\trole: \"toolResult\",\n\ttoolCallId: \"tool_xyz\",\n\ttoolName: \"generate_chart\",\n\tcontent: [\n\t\t{ type: \"text\", text: \"Generated chart showing temperature trends\" },\n\t\t{ type: \"image\", data: imageBuffer.toBase64(), mimeType: \"image/png\" },\n\t],\n\tisError: false,\n\ttimestamp: Date.now(),\n});\n```\n\n### Streaming Tool Calls with Partial JSON\n\nDuring streaming, tool call arguments are progressively parsed as they arrive. This enables real-time UI updates before the complete arguments are available:\n\n```typescript\nconst s = stream(model, context);\n\nfor await (const event of s) {\n\tif (event.type === \"toolcall_delta\") {\n\t\tconst toolCall = event.partial.content[event.contentIndex];\n\n\t\t// toolCall.arguments contains partially parsed JSON during streaming\n\t\t// This allows for progressive UI updates\n\t\tif (toolCall.type === \"toolCall\" && toolCall.arguments) {\n\t\t\t// BE DEFENSIVE: arguments may be incomplete\n\t\t\t// Example: Show file path being written even before content is complete\n\t\t\tif (toolCall.name === \"write_file\" && toolCall.arguments.path) {\n\t\t\t\tconsole.log(`Writing to: ${toolCall.arguments.path}`);\n\n\t\t\t\t// Content might be partial or missing\n\t\t\t\tif (toolCall.arguments.content) {\n\t\t\t\t\tconsole.log(`Content preview: ${toolCall.arguments.content.substring(0, 100)}...`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (event.type === \"toolcall_end\") {\n\t\t// Here toolCall.arguments is complete (but not yet validated)\n\t\tconst toolCall = event.toolCall;\n\t\tconsole.log(`Tool completed: ${toolCall.name}`, toolCall.arguments);\n\t}\n}\n```\n\n**Important notes about partial tool arguments:**\n\n- During `toolcall_delta` events, `arguments` contains the best-effort parse of partial JSON\n- Fields may be missing or incomplete - always check for existence before use\n- String values may be truncated mid-word\n- Arrays may be incomplete\n- Nested objects may be partially populated\n- At minimum, `arguments` will be an empty object `{}`, never `undefined`\n- The Google provider does not support function call streaming. Instead, you will receive a single `toolcall_delta` event with the full arguments.\n\n### Validating Tool Arguments\n\nWhen using `agentLoop`, tool arguments are automatically validated against your Zod parameter schemas before execution. If validation fails, the error is returned to the model as a tool result, allowing it to retry.\n\nWhen implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:\n\n```typescript\nimport { stream, validateToolCall, Tool } from \"@oh-my-pi/pi-ai\";\n\nconst tools: Tool[] = [weatherTool, calculatorTool];\nconst s = stream(model, { messages, tools });\n\nfor await (const event of s) {\n\tif (event.type === \"toolcall_end\") {\n\t\tconst toolCall = event.toolCall;\n\n\t\ttry {\n\t\t\t// Validate arguments against the tool's schema (throws on invalid args)\n\t\t\tconst validatedArgs = validateToolCall(tools, toolCall);\n\t\t\tconst result = await executeMyTool(toolCall.name, validatedArgs);\n\t\t\t// ... add tool result to context\n\t\t} catch (error) {\n\t\t\t// Validation failed - return error as tool result so model can retry\n\t\t\tcontext.messages.push({\n\t\t\t\trole: \"toolResult\",\n\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\ttoolName: toolCall.name,\n\t\t\t\tcontent: [{ type: \"text\", text: error.message }],\n\t\t\t\tisError: true,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t});\n\t\t}\n\t}\n}\n```\n\n### Complete Event Reference\n\nAll streaming events emitted during assistant message generation:\n\n| Event Type | Description | Key Properties |\n| ---------------- | ------------------------ | ------------------------------------------------------------------------------------------- |\n| `start` | Stream begins | `partial`: Initial assistant message structure |\n| `text_start` | Text block starts | `contentIndex`: Position in content array |\n| `text_delta` | Text chunk received | `delta`: New text, `contentIndex`: Position |\n| `text_end` | Text block complete | `content`: Full text, `contentIndex`: Position |\n| `thinking_start` | Thinking block starts | `contentIndex`: Position in content array |\n| `thinking_delta` | Thinking chunk received | `delta`: New text, `contentIndex`: Position |\n| `thinking_end` | Thinking block complete | `content`: Full thinking, `contentIndex`: Position |\n| `toolcall_start` | Tool call begins | `contentIndex`: Position in content array |\n| `toolcall_delta` | Tool arguments streaming | `delta`: JSON chunk, `partial.content[contentIndex].arguments`: Partial parsed args |\n| `toolcall_end` | Tool call complete | `toolCall`: Complete validated tool call with `id`, `name`, `arguments` |\n| `done` | Stream complete | `reason`: Stop reason (\"stop\", \"length\", \"toolUse\"), `message`: Final assistant message |\n| `error` | Error occurred | `reason`: Error type (\"error\" or \"aborted\"), `error`: AssistantMessage with partial content |\n\n## Image Input\n\nModels with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.\n\n```typescript\nimport * as fs from \"node:fs\";\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Check if model supports images\nif (model.input.includes(\"image\")) {\n\tconsole.log(\"Model supports vision\");\n}\n\nconst imageBuffer = fs.readFileSync(\"image.png\");\nconst base64Image = imageBuffer.toBase64();\n\nconst response = await complete(model, {\n\tmessages: [\n\t\t{\n\t\t\trole: \"user\",\n\t\t\tcontent: [\n\t\t\t\t{ type: \"text\", text: \"What is in this image?\" },\n\t\t\t\t{ type: \"image\", data: base64Image, mimeType: \"image/png\" },\n\t\t\t],\n\t\t},\n\t],\n});\n\n// Access the response\nfor (const block of response.content) {\n\tif (block.type === \"text\") {\n\t\tconsole.log(block.text);\n\t}\n}\n```\n\n## Thinking/Reasoning\n\nMany models support thinking/reasoning capabilities where they can show their internal thought process. You can check if a model supports reasoning via the `reasoning` property. If you pass reasoning options to a non-reasoning model, they are silently ignored.\n\n### Unified Interface (streamSimple/completeSimple)\n\n```typescript\nimport { getModel, streamSimple, completeSimple } from \"@oh-my-pi/pi-ai\";\n\n// Many models across providers support thinking/reasoning\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\n// or getModel('openai', 'gpt-5-mini');\n// or getModel('google', 'gemini-2.5-flash');\n// or getModel('xai', 'grok-code-fast-1');\n// or getModel('groq', 'openai/gpt-oss-20b');\n// or getModel('cerebras', 'gpt-oss-120b');\n// or getModel('openrouter', 'z-ai/glm-4.5v');\n\n// Check if model supports reasoning\nif (model.reasoning) {\n\tconsole.log(\"Model supports reasoning/thinking\");\n}\n\n// Use the simplified reasoning option\nconst response = await completeSimple(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Solve: 2x + 5 = 13\" }],\n\t},\n\t{\n\t\treasoning: \"medium\", // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers)\n\t}\n);\n\n// Access thinking and text blocks\nfor (const block of response.content) {\n\tif (block.type === \"thinking\") {\n\t\tconsole.log(\"Thinking:\", block.thinking);\n\t} else if (block.type === \"text\") {\n\t\tconsole.log(\"Response:\", block.text);\n\t}\n}\n```\n\n### Provider-Specific Options (stream/complete)\n\nFor fine-grained control, use the provider-specific options:\n\n```typescript\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n// OpenAI Reasoning (o1, o3, gpt-5)\nconst openaiModel = getModel(\"openai\", \"gpt-5-mini\");\nawait complete(openaiModel, context, {\n\treasoningEffort: \"medium\",\n\treasoningSummary: \"detailed\", // OpenAI Responses API only\n});\n\n// Anthropic Thinking (Claude Sonnet 4)\nconst anthropicModel = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\nawait complete(anthropicModel, context, {\n\tthinkingEnabled: true,\n\tthinkingBudgetTokens: 8192, // Optional token limit\n});\n\n// Google Gemini Thinking\nconst googleModel = getModel(\"google\", \"gemini-2.5-flash\");\nawait complete(googleModel, context, {\n\tthinking: {\n\t\tenabled: true,\n\t\tbudgetTokens: 8192, // -1 for dynamic, 0 to disable\n\t},\n});\n```\n\n### Streaming Thinking Content\n\nWhen streaming, thinking content is delivered through specific events:\n\n```typescript\nconst s = streamSimple(model, context, { reasoning: \"high\" });\n\nfor await (const event of s) {\n\tswitch (event.type) {\n\t\tcase \"thinking_start\":\n\t\t\tconsole.log(\"[Model started thinking]\");\n\t\t\tbreak;\n\t\tcase \"thinking_delta\":\n\t\t\tprocess.stdout.write(event.delta); // Stream thinking content\n\t\t\tbreak;\n\t\tcase \"thinking_end\":\n\t\t\tconsole.log(\"\\n[Thinking complete]\");\n\t\t\tbreak;\n\t}\n}\n```\n\n## Stop Reasons\n\nEvery `AssistantMessage` includes a `stopReason` field that indicates how the generation ended:\n\n- `\"stop\"` - Normal completion, the model finished its response\n- `\"length\"` - Output hit the maximum token limit\n- `\"toolUse\"` - Model is calling tools and expects tool results\n- `\"error\"` - An error occurred during generation\n- `\"aborted\"` - Request was cancelled via abort signal\n\n## Error Handling\n\nWhen a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event:\n\n```typescript\n// In streaming\nfor await (const event of stream) {\n\tif (event.type === \"error\") {\n\t\t// event.reason is either \"error\" or \"aborted\"\n\t\t// event.error is the AssistantMessage with partial content\n\t\tconsole.error(`Error (${event.reason}):`, event.error.errorMessage);\n\t\tconsole.log(\"Partial content:\", event.error.content);\n\t}\n}\n\n// The final message will have the error details\nconst message = await stream.result();\nif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\tconsole.error(\"Request failed:\", message.errorMessage);\n\t// message.content contains any partial content received before the error\n\t// message.usage contains partial token counts and costs\n}\n```\n\n### Aborting Requests\n\nThe abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:\n\n```typescript\nimport { getModel, stream } from \"@oh-my-pi/pi-ai\";\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Abort after 2 seconds\nconst signal = AbortSignal.timeout(2000);\n\nconst s = stream(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Write a long story\" }],\n\t},\n\t{\n\t\tsignal,\n\t}\n);\n\nfor await (const event of s) {\n\tif (event.type === \"text_delta\") {\n\t\tprocess.stdout.write(event.delta);\n\t} else if (event.type === \"error\") {\n\t\t// event.reason tells you if it was \"error\" or \"aborted\"\n\t\tconsole.log(`${event.reason === \"aborted\" ? \"Aborted\" : \"Error\"}:`, event.error.errorMessage);\n\t}\n}\n\n// Get results (may be partial if aborted)\nconst response = await s.result();\nif (response.stopReason === \"aborted\") {\n\tconsole.log(\"Request was aborted:\", response.errorMessage);\n\tconsole.log(\"Partial content received:\", response.content);\n\tconsole.log(\"Tokens used:\", response.usage);\n}\n```\n\n### Continuing After Abort\n\nAborted messages can be added to the conversation context and continued in subsequent requests:\n\n```typescript\nconst context = {\n\tmessages: [{ role: \"user\", content: \"Explain quantum computing in detail\" }],\n};\n\n// First request gets aborted after 2 seconds\nconst controller1 = new AbortController();\nsetTimeout(() => controller1.abort(), 2000);\n\nconst partial = await complete(model, context, { signal: controller1.signal });\n\n// Add the partial response to context\ncontext.messages.push(partial);\ncontext.messages.push({ role: \"user\", content: \"Please continue\" });\n\n// Continue the conversation\nconst continuation = await complete(model, context);\n```\n\n### Common Stream Options\n\nAll providers accept the base `StreamOptions` (in addition to provider-specific options):\n\n- `apiKey`: Override the provider API key\n- `headers`: Extra request headers merged on top of model-defined headers\n- `sessionId`: Provider-specific session identifier (prompt caching/routing)\n- `signal`: Abort in-flight requests\n- `onPayload`: Callback invoked with the provider request payload just before sending\n\nExample:\n\n```typescript\nconst response = await complete(model, context, {\n\tapiKey: \"sk-live\",\n\theaders: { \"X-Debug-Trace\": \"true\" },\n\tonPayload: (payload) => {\n\t\tconsole.log(\"request payload\", payload);\n\t},\n});\n```\n\n## APIs, Models, and Providers\n\nThe library implements 4 API interfaces, each with its own streaming function and options:\n\n- **`anthropic-messages`**: Anthropic's Messages API (`streamAnthropic`, `AnthropicOptions`)\n- **`google-generative-ai`**: Google's Generative AI API (`streamGoogle`, `GoogleOptions`)\n- **`openai-completions`**: OpenAI's Chat Completions API (`streamOpenAICompletions`, `OpenAICompletionsOptions`)\n- **`openai-responses`**: OpenAI's Responses API (`streamOpenAIResponses`, `OpenAIResponsesOptions`)\n\n### Providers and Models\n\nA **provider** offers models through a specific API. For example:\n\n- **Anthropic** models use the `anthropic-messages` API\n- **Google** models use the `google-generative-ai` API\n- **OpenAI** models use the `openai-responses` API\n- **Mistral, xAI, Cerebras, Groq, etc.** models use the `openai-completions` API (OpenAI-compatible)\n\n### Querying Providers and Models\n\n```typescript\nimport { getProviders, getModels, getModel } from \"@oh-my-pi/pi-ai\";\n\n// Get all available providers\nconst providers = getProviders();\nconsole.log(providers); // ['openai', 'anthropic', 'google', 'xai', 'groq', ...]\n\n// Get all models from a provider (fully typed)\nconst anthropicModels = getModels(\"anthropic\");\nfor (const model of anthropicModels) {\n\tconsole.log(`${model.id}: ${model.name}`);\n\tconsole.log(` API: ${model.api}`); // 'anthropic-messages'\n\tconsole.log(` Context: ${model.contextWindow} tokens`);\n\tconsole.log(` Vision: ${model.input.includes(\"image\")}`);\n\tconsole.log(` Reasoning: ${model.reasoning}`);\n}\n\n// Get a specific model (both provider and model ID are auto-completed in IDEs)\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\nconsole.log(`Using ${model.name} via ${model.api} API`);\n```\n\n### Custom Models\n\nYou can create custom models for local inference servers or custom endpoints.\n\nFor local Ollama, `OLLAMA_API_KEY` is optional and mainly needed for authenticated/self-hosted gateways. `ollama` remains the local OpenAI-compatible runtime integration.\n\n```typescript\nimport { Model, stream } from \"@oh-my-pi/pi-ai\";\n\n// Example: local Ollama using the OpenAI-compatible API\nconst ollamaModel: Model<\"openai-completions\"> = {\n\tid: \"llama-3.1-8b\",\n\tname: \"Llama 3.1 8B (Ollama)\",\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 32000,\n};\n\nconst localResponse = await stream(ollamaModel, context, {\n\tapiKey: process.env.OLLAMA_API_KEY, // Optional; local Ollama usually runs without auth\n});\n\n// Example: Ollama Cloud using the native /api/chat transport\nconst ollamaCloudModel: Model<\"ollama-chat\"> = {\n\tid: \"gpt-oss:120b\",\n\tname: \"GPT OSS 120B (Ollama Cloud)\",\n\tapi: \"ollama-chat\",\n\tprovider: \"ollama-cloud\",\n\tbaseUrl: \"https://ollama.com\",\n\treasoning: true,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 262144,\n\tmaxTokens: 8192,\n};\n\nconst cloudResponse = await stream(ollamaCloudModel, context, {\n\tapiKey: process.env.OLLAMA_CLOUD_API_KEY,\n});\n\n// Example: LiteLLM proxy with explicit compat settings\nconst litellmModel: Model<\"openai-completions\"> = {\n\tid: \"gpt-4o\",\n\tname: \"GPT-4o (via LiteLLM)\",\n\tapi: \"openai-completions\",\n\tprovider: \"litellm\",\n\tbaseUrl: \"http://localhost:4000/v1\",\n\treasoning: false,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 16384,\n\tcompat: {\n\t\tsupportsStore: false, // LiteLLM doesn't support the store field\n\t},\n};\n\n// Example: Custom endpoint with headers (bypassing Cloudflare bot detection)\nconst proxyModel: Model<\"anthropic-messages\"> = {\n\tid: \"claude-sonnet-4\",\n\tname: \"Claude Sonnet 4 (Proxied)\",\n\tapi: \"anthropic-messages\",\n\tprovider: \"custom-proxy\",\n\tbaseUrl: \"https://proxy.example.com/v1\",\n\treasoning: true,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },\n\tcontextWindow: 200000,\n\tmaxTokens: 8192,\n\theaders: {\n\t\t\"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36\",\n\t\t\"X-Custom-Auth\": \"bearer-token-here\",\n\t},\n};\n```\n\n### OpenAI Compatibility Settings\n\nThe `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for known providers (Cerebras, xAI, Mistral, Chutes, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field:\n\n```typescript\ninterface OpenAICompat {\n\tsupportsStore?: boolean; // Whether provider supports the `store` field (default: true)\n\tsupportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)\n\tsupportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)\n\tmaxTokensField?: \"max_completion_tokens\" | \"max_tokens\"; // Which field name to use (default: max_completion_tokens)\n\textraBody?: Record<string, unknown>; // Extra request-body fields for custom proxy routing or provider-specific options\n}\n```\n\nIf `compat` is not set, the library falls back to URL-based detection. If `compat` is partially set, unspecified fields use the detected defaults. This is useful for:\n\n- **LiteLLM proxies**: May not support `store` field\n- **Custom inference servers**: May use non-standard field names\n- **Self-hosted endpoints**: May have different feature support\n\n### Type Safety\n\nModels are typed by their API, ensuring type-safe options:\n\n```typescript\n// TypeScript knows this is an Anthropic model\nconst claude = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\n\n// So these options are type-checked for AnthropicOptions\nawait stream(claude, context, {\n\tthinkingEnabled: true, // ✓ Valid for anthropic-messages\n\tthinkingBudgetTokens: 2048, // ✓ Valid for anthropic-messages\n\t// reasoningEffort: 'high' // ✗ TypeScript error: not valid for anthropic-messages\n});\n```\n\n## Cross-Provider Handoffs\n\nThe library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including thinking blocks, tool calls, and tool results.\n\n### How It Works\n\nWhen messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:\n\n- **User and tool result messages** are passed through unchanged\n- **Assistant messages from the same provider/API** are preserved as-is\n- **Assistant messages from different providers** have their thinking blocks converted to text with `<thinking>` tags\n- **Tool calls and regular text** are preserved unchanged\n\n### Example: Multi-Provider Conversation\n\n```typescript\nimport { getModel, complete, Context } from \"@oh-my-pi/pi-ai\";\n\n// Start with Claude\nconst claude = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\nconst context: Context = {\n\tmessages: [],\n};\n\ncontext.messages.push({ role: \"user\", content: \"What is 25 * 18?\" });\nconst claudeResponse = await complete(claude, context, {\n\tthinkingEnabled: true,\n});\ncontext.messages.push(claudeResponse);\n\n// Switch to GPT-5 - it will see Claude's thinking as <thinking> tagged text\nconst gpt5 = getModel(\"openai\", \"gpt-5-mini\");\ncontext.messages.push({ role: \"user\", content: \"Is that calculation correct?\" });\nconst gptResponse = await complete(gpt5, context);\ncontext.messages.push(gptResponse);\n\n// Switch to Gemini\nconst gemini = getModel(\"google\", \"gemini-2.5-flash\");\ncontext.messages.push({ role: \"user\", content: \"What was the original question?\" });\nconst geminiResponse = await complete(gemini, context);\n```\n\n### Provider Compatibility\n\nAll providers can handle messages from other providers, including:\n\n- Text content\n- Tool calls and tool results (including images in tool results)\n- Thinking/reasoning blocks (transformed to tagged text for cross-provider compatibility)\n- Aborted messages with partial content\n\nThis enables flexible workflows where you can:\n\n- Start with a fast model for initial responses\n- Switch to a more capable model for complex reasoning\n- Use specialized models for specific tasks\n- Maintain conversation continuity across provider outages\n\n## Context Serialization\n\nThe `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:\n\n```typescript\nimport { Context, getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n// Create and use a context\nconst context: Context = {\n\tsystemPrompt: [\"You are a helpful assistant.\"],\n\tmessages: [{ role: \"user\", content: \"What is TypeScript?\" }],\n};\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\nconst response = await complete(model, context);\ncontext.messages.push(response);\n\n// Serialize the entire context\nconst serialized = JSON.stringify(context);\nconsole.log(\"Serialized context size:\", serialized.length, \"bytes\");\n\n// Save to database, localStorage, file, etc.\nlocalStorage.setItem(\"conversation\", serialized);\n\n// Later: deserialize and continue the conversation\nconst restored: Context = JSON.parse(localStorage.getItem(\"conversation\")!);\nrestored.messages.push({ role: \"user\", content: \"Tell me more about its type system\" });\n\n// Continue with any model\nconst newModel = getModel(\"anthropic\", \"claude-haiku-4-5-20251001\");\nconst continuation = await complete(newModel, restored);\n```\n\n> **Note**: If the context contains images (encoded as base64 as shown in the Image Input section), those will also be serialized.\n\n## Browser Usage\n\nThe library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers:\n\n```typescript\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n// API key must be passed explicitly in browser\nconst model = getModel(\"anthropic\", \"claude-haiku-4-5-20251001\");\n\nconst response = await complete(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Hello!\" }],\n\t},\n\t{\n\t\tapiKey: \"your-api-key\",\n\t}\n);\n```\n\n> **Security Warning**: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys. Only use this approach for internal tools or demos. For production applications, use a backend proxy that keeps your API keys secure.\n\n### Environment Variables (Node.js only)\n\nIn Node.js environments, you can set environment variables to avoid passing API keys:\n\n| Provider | Environment Variable(s) |\n| -------------- | ---------------------------------------------------------------------------- |\n| OpenAI | `OPENAI_API_KEY` |\n| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` (or `ANTHROPIC_FOUNDRY_API_KEY` when `CLAUDE_CODE_USE_FOUNDRY=true`) |\n| Google | `GEMINI_API_KEY` |\n| Vertex AI | `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |\n| Mistral | `MISTRAL_API_KEY` |\n| Groq | `GROQ_API_KEY` |\n| Cerebras | `CEREBRAS_API_KEY` |\n| Together | `TOGETHER_API_KEY` |\n| Qianfan | `QIANFAN_API_KEY` |\n| Hugging Face | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` |\n| Synthetic | `SYNTHETIC_API_KEY` |\n| NVIDIA | `NVIDIA_API_KEY` |\n| NanoGPT | `NANO_GPT_API_KEY` |\n| Novita | `NOVITA_API_KEY` |\n| Venice | `VENICE_API_KEY` |\n| Moonshot | `MOONSHOT_API_KEY` |\n| xAI | `XAI_API_KEY` |\n| OpenRouter | `OPENROUTER_API_KEY` |\n| LiteLLM | `LITELLM_API_KEY` |\n| Ollama | `OLLAMA_API_KEY` (optional for local deployments) |\n| Ollama Cloud | `OLLAMA_CLOUD_API_KEY` |\n| Qwen Portal | `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY` |\n| QwenCloud Token Plan | `ALIBABA_TOKEN_PLAN_API_KEY` or `BAILIAN_TOKEN_PLAN_API_KEY` |\n| zAI | `ZAI_API_KEY` |\n| Umans AI Coding Plan | `UMANS_AI_CODING_PLAN_API_KEY` |\n| MiniMax Code | `MINIMAX_CODE_API_KEY` (international) or `MINIMAX_CODE_CN_API_KEY` (China) |\n| Xiaomi MiMo | `XIAOMI_API_KEY` |\n| ZenMux | `ZENMUX_API_KEY` |\n| vLLM | `VLLM_API_KEY` |\n| Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY` |\n| GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |\n\nFor Cloudflare AI Gateway models, use provider base URL format\n`https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`.\n\nFor Anthropic Foundry routing, set `CLAUDE_CODE_USE_FOUNDRY=true` plus:\n`FOUNDRY_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, optional `ANTHROPIC_CUSTOM_HEADERS`,\nand optional mTLS material (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`).\n\n`NODE_EXTRA_CA_CERTS` (PEM file path or inline PEM, mirroring Node's contract)\nis honoured on every provider fetch — OpenAI-compatible, Codex, Ollama, Azure\nResponses, Google, and Anthropic alike — for corporate relays or private CA\nbundles. Bun's `fetch` does not consume the env var natively, so omp injects\nthe bundle into `RequestInit.tls.ca` and seeds the system root store\nalongside it.\n\nProvider endpoint defaults for the current OpenAI-compatible integrations:\n\n- Together: `https://api.together.xyz/v1`\n- Moonshot: `https://api.moonshot.ai/v1`\n- Qianfan: `https://qianfan.baidubce.com/v2`\n- NVIDIA: `https://integrate.api.nvidia.com/v1`\n- NanoGPT: `https://nano-gpt.com/api/v1`\n- Novita: `https://api.novita.ai/openai/v1`\n- Hugging Face Inference: `https://router.huggingface.co/v1`\n- Venice: `https://api.venice.ai/api/v1`\n- Xiaomi MiMo: `https://api.xiaomimimo.com/anthropic`\n- ZenMux (OpenAI): `https://zenmux.ai/api/v1`\n- ZenMux (Anthropic models): `https://zenmux.ai/api/anthropic`\n- Umans AI Coding Plan: `https://api.code.umans.ai`\n- vLLM: `http://127.0.0.1:8000/v1`\n- Ollama: local OpenAI-compatible runtime (`http://127.0.0.1:11434/v1`)\n- Ollama Cloud: native Ollama API host (`https://ollama.com/api`, configured here as base URL `https://ollama.com`)\n- LiteLLM: `http://localhost:4000/v1`\n- Cloudflare AI Gateway: `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`\n- Qwen Portal: `https://portal.qwen.ai/v1`\nWhen set, the library automatically uses these keys:\n\n```typescript\n// Uses OPENAI_API_KEY from environment\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\nconst response = await complete(model, context);\n\n// Or override with explicit key\nconst response = await complete(model, context, {\n\tapiKey: \"sk-different-key\",\n});\n```\n\n### Checking Environment Variables\n\n```typescript\nimport { getEnvApiKey } from \"@oh-my-pi/pi-ai\";\n\n// Check if an API key is set in environment variables\nconst key = getEnvApiKey(\"openai\"); // checks OPENAI_API_KEY\n```\n\n## OAuth Providers\n\nSeveral providers support OAuth authentication (some also support static API keys):\n\n- **Anthropic** (Claude Pro/Max subscription)\n- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)\n- **GitHub Copilot** (Copilot subscription)\n- **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)\n- **Antigravity** (Free Gemini 3, Claude, GPT-OSS via Google Cloud)\n- **Qwen Portal** (Qwen OAuth token or API key)\n\nFor paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID.\n\n### Vertex AI (ADC)\n\nVertex AI models use Application Default Credentials (ADC):\n\n- **Local development**: Run `gcloud auth application-default login`\n- **CI/Production**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to a service account JSON key file\n\nAlso set `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) and `GOOGLE_CLOUD_LOCATION`. You can also pass `project`/`location` in the call options.\n\nExample:\n\n```bash\n# Local (uses your user credentials)\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=\"my-project\"\nexport GOOGLE_CLOUD_LOCATION=\"us-central1\"\n\n# CI/Production (service account key file)\nexport GOOGLE_APPLICATION_CREDENTIALS=\"/path/to/service-account.json\"\n```\n\n```typescript\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n(async () => {\n\tconst model = getModel(\"google-vertex\", \"gemini-2.5-flash\");\n\tconst response = await complete(model, {\n\t\tmessages: [{ role: \"user\", content: \"Hello from Vertex AI\" }],\n\t});\n\n\tfor (const block of response.content) {\n\t\tif (block.type === \"text\") console.log(block.text);\n\t}\n})().catch(console.error);\n```\n\nOfficial docs: [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials)\n\n### CLI Login\n\nAuthenticate via the [`omp`](https://omp.sh) coding-agent CLI, which drives this library's OAuth/API-key flows in-process and persists into `agent.db`:\n\n```bash\nomp auth-broker login # interactive provider selection\nomp auth-broker login anthropic # login to a specific provider\nomp auth-broker login vllm # store vLLM API key (or placeholder for local no-auth)\nomp auth-broker list # list supported providers\nomp auth-broker logout # interactive — pick a stored credential to remove\n```\n\nCredentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.\n\n`login` supports OAuth providers (Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.\n\nFor the current API-key onboarding flows, the library covers Together, Moonshot, Qianfan, NVIDIA, NanoGPT, Novita, Hugging Face, Venice, Xiaomi, vLLM, LiteLLM, Cloudflare AI Gateway, Qwen Portal, and Ollama Cloud. Ollama remains the local runtime integration; set `OLLAMA_API_KEY` only when your local or self-hosted deployment enforces bearer auth.\n\n### Programmatic OAuth\n\nThe library provides login and token refresh functions. Credential storage is the caller's responsibility.\n\n```typescript\nimport {\n\t// Login functions (return credentials, do not store)\n\tloginAnthropic,\n\tloginOpenAICodex,\n\tloginGitHubCopilot,\n\tloginGeminiCli,\n\tloginAntigravity,\n\tloginCloudflareAiGateway,\n\tloginHuggingface,\n\tloginLiteLLM,\n\tloginMoonshot,\n\tloginNvidia,\n\tloginNanoGPT,\n\tloginQianfan,\n\tloginQwenPortal,\n\tloginTogether,\n\tloginVenice,\n\tloginVllm,\n\tloginXiaomi,\n\n\t// Token management\n\trefreshOAuthToken, // (provider, credentials) => new credentials\n\tgetOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null\n\n\t// Types\n\ttype OAuthProvider, // includes 'anthropic', 'openai-codex', 'github-copilot', 'google-gemini-cli', 'google-antigravity', 'together', 'moonshot', 'qianfan', 'nvidia', 'nanogpt', 'novita', 'huggingface', 'venice', 'xiaomi', 'vllm', 'litellm', 'cloudflare-ai-gateway', 'qwen-portal', ...\n\ttype OAuthCredentials,\n} from \"@oh-my-pi/pi-ai\";\n```\n\n`loginOpenAICodex` accepts an optional `originator` value used in the OAuth flow:\n\n```typescript\nawait loginOpenAICodex({\n\tonAuth: ({ url }) => console.log(url),\n\toriginator: \"my-cli\",\n});\n```\n\n### Login Flow Example\n\n```typescript\nimport { loginGitHubCopilot } from \"@oh-my-pi/pi-ai\";\nimport * as fs from \"node:fs\";\n\nconst credentials = await loginGitHubCopilot({\n\tonAuth: (url, instructions) => {\n\t\tconsole.log(`Open: ${url}`);\n\t\tif (instructions) console.log(instructions);\n\t},\n\tonPrompt: async (prompt) => {\n\t\treturn await getUserInput(prompt.message);\n\t},\n\tonProgress: (message) => console.log(message),\n});\n\n// Store credentials yourself\nconst auth = { \"github-copilot\": { type: \"oauth\", ...credentials } };\nfs.writeFileSync(\"credentials.json\", JSON.stringify(auth, null, 2));\n```\n\n### Using OAuth Tokens\n\nUse `getOAuthApiKey()` to get an API key, automatically refreshing if expired:\n\n```typescript\nimport { getModel, complete, getOAuthApiKey } from \"@oh-my-pi/pi-ai\";\nimport * as fs from \"node:fs\";\n\n// Load your stored credentials\nconst auth = JSON.parse(fs.readFileSync(\"credentials.json\", \"utf-8\"));\n\n// Get API key (refreshes if expired)\nconst result = await getOAuthApiKey(\"github-copilot\", auth);\nif (!result) throw new Error(\"Not logged in\");\n\n// Save refreshed credentials\nauth[\"github-copilot\"] = { type: \"oauth\", ...result.newCredentials };\nfs.writeFileSync(\"credentials.json\", JSON.stringify(auth, null, 2));\n\n// Use the API key\nconst model = getModel(\"github-copilot\", \"gpt-4o\");\nconst response = await complete(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Hello!\" }],\n\t},\n\t{ apiKey: result.apiKey }\n);\n```\n\n### Provider Notes\n\n**OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options.\n\n**GitHub Copilot**: If you get \"The requested model is not supported\" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click \"Enable\".\n\n**Google Gemini CLI / Antigravity**: These use Google Cloud OAuth. The `apiKey` returned by `getOAuthApiKey()` is a JSON string containing both the token and project ID, which the library handles automatically.\n\n## License\n\nMIT\n"}, "003bb5548c7c1e759336e264305790de435708d1": {"t": 1785846922.6762002, "n": 45340, "h": "8aec5ecad840718a", "c": "# @oh-my-pi/pi-ai\n\nUnified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session.\n\n**Note**: This library only includes models that support tool calling (function calling), as this is essential for agentic workflows.\n\n## Table of Contents\n\n- [Supported Providers](#supported-providers)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Tools](#tools)\n - [Defining Tools](#defining-tools)\n - [Handling Tool Calls](#handling-tool-calls)\n - [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)\n - [Validating Tool Arguments](#validating-tool-arguments)\n - [Complete Event Reference](#complete-event-reference)\n- [Image Input](#image-input)\n- [Thinking/Reasoning](#thinkingreasoning)\n - [Unified Interface](#unified-interface-streamsimplecompletesimple)\n - [Provider-Specific Options](#provider-specific-options-streamcomplete)\n - [Streaming Thinking Content](#streaming-thinking-content)\n- [Stop Reasons](#stop-reasons)\n- [Error Handling](#error-handling)\n - [Aborting Requests](#aborting-requests)\n - [Continuing After Abort](#continuing-after-abort)\n- [APIs, Models, and Providers](#apis-models-and-providers)\n - [Providers and Models](#providers-and-models)\n - [Querying Providers and Models](#querying-providers-and-models)\n - [Custom Models](#custom-models)\n - [OpenAI Compatibility Settings](#openai-compatibility-settings)\n - [Type Safety](#type-safety)\n- [Cross-Provider Handoffs](#cross-provider-handoffs)\n- [Context Serialization](#context-serialization)\n- [Browser Usage](#browser-usage)\n - [Environment Variables](#environment-variables-nodejs-only)\n - [Checking Environment Variables](#checking-environment-variables)\n- [OAuth Providers](#oauth-providers)\n - [Vertex AI (ADC)](#vertex-ai-adc)\n - [CLI Login](#cli-login)\n - [Programmatic OAuth](#programmatic-oauth)\n - [Login Flow Example](#login-flow-example)\n - [Using OAuth Tokens](#using-oauth-tokens)\n - [Provider Notes](#provider-notes)\n- [License](#license)\n\n## Supported Providers\n\n- **OpenAI**\n- **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)\n- **Anthropic**\n- **Google**\n- **Vertex AI** (Gemini via Vertex AI)\n- **Mistral**\n- **Groq**\n- **Cerebras**\n- **Together**\n- **Moonshot** (requires `MOONSHOT_API_KEY`)\n- **Qianfan** (requires `QIANFAN_API_KEY`)\n- **NVIDIA** (requires `NVIDIA_API_KEY`)\n- **NanoGPT** (requires `NANO_GPT_API_KEY`)\n- **Novita** (requires `NOVITA_API_KEY`)\n- **Hugging Face Inference**\n- **xAI**\n- **Venice** (requires `VENICE_API_KEY`)\n- **Wafer Serverless** (requires `WAFER_SERVERLESS_API_KEY`; pay-as-you-go)\n- **OpenRouter**\n- **Kilo Gateway** (supports OAuth `/login kilo` or `KILO_API_KEY`)\n- **LiteLLM** (requires `LITELLM_API_KEY`)\n- **zAI** (requires `ZAI_API_KEY`)\n- **Umans AI Coding Plan** (supports `/login umans` or `UMANS_AI_CODING_PLAN_API_KEY`)\n- **MiniMax Token Plan** (requires `MINIMAX_CODE_API_KEY` or `MINIMAX_CODE_CN_API_KEY`)\n- **Xiaomi MiMo** (requires `XIAOMI_API_KEY`)\n- **ZenMux** (requires `ZENMUX_API_KEY`)\n- **Qwen Portal** (supports `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY`)\n- **QwenCloud Token Plan** (supports `/login alibaba-token-plan`, `ALIBABA_TOKEN_PLAN_API_KEY`, or `BAILIAN_TOKEN_PLAN_API_KEY`; interactive login first selects a region — International (Singapore, default), China (Beijing) for 百炼 Token Plan keys, or a custom base URL — since region keys are non-interchangeable, then optionally stores a `home.qwencloud.com` Cookie request header for best-effort 5-hour and 7-day quota reporting)\n To enable quota reporting, sign in to the Token Plan dashboard, copy the `Cookie` request-header value from a `home.qwencloud.com` request in browser developer tools, and paste it at the second login prompt. Press Enter to skip; the Cookie is sensitive and session-lived, so rerun login when it expires.\n- **Cloudflare AI Gateway** (requires `CLOUDFLARE_AI_GATEWAY_API_KEY` and provider-specific gateway base URL)\n- **Ollama** (local OpenAI-compatible runtime; optional `OLLAMA_API_KEY`)\n- **Ollama Cloud** (hosted native Ollama API; requires `OLLAMA_CLOUD_API_KEY`)\n- **llama.cpp** (local OpenAI and Anthropic compatible inference server)\n- **vLLM** (OpenAI-compatible server; `VLLM_API_KEY` for secured deployments)\n- **GitHub Copilot** (requires OAuth, see below)\n- **Google Gemini CLI** (requires OAuth, see below)\n- **Antigravity** (requires OAuth, see below)\n- **Any OpenAI-compatible API**: LM Studio, custom proxies, etc.\n\n## Installation\n\n```bash\nnpm install @oh-my-pi/pi-ai\n```\n\n## Quick Start\n\n```typescript\nimport { z, getModel, stream, complete, Context, Tool } from \"@oh-my-pi/pi-ai\";\n\n// Fully typed with auto-complete support for both providers and models\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Define tools with Zod schemas for type safety and validation\nconst tools: Tool[] = [\n\t{\n\t\tname: \"get_time\",\n\t\tdescription: \"Get the current time\",\n\t\tparameters: z.object({\n\t\t\ttimezone: z\n\t\t\t\t.string()\n\t\t\t\t.optional()\n\t\t\t\t.describe(\"Optional timezone (e.g., America/New_York)\"),\n\t\t}),\n\t},\n];\n\n// Build a conversation context (easily serializable and transferable between models)\nconst context: Context = {\n\tsystemPrompt: [\"You are a helpful assistant.\"],\n\tmessages: [{ role: \"user\", content: \"What time is it?\" }],\n\ttools,\n};\n\n// Option 1: Streaming with all event types\nconst s = stream(model, context);\n\nfor await (const event of s) {\n\tswitch (event.type) {\n\t\tcase \"start\":\n\t\t\tconsole.log(`Starting with ${event.partial.model}`);\n\t\t\tbreak;\n\t\tcase \"text_start\":\n\t\t\tconsole.log(\"\\n[Text started]\");\n\t\t\tbreak;\n\t\tcase \"text_delta\":\n\t\t\tprocess.stdout.write(event.delta);\n\t\t\tbreak;\n\t\tcase \"text_end\":\n\t\t\tconsole.log(\"\\n[Text ended]\");\n\t\t\tbreak;\n\t\tcase \"thinking_start\":\n\t\t\tconsole.log(\"[Model is thinking...]\");\n\t\t\tbreak;\n\t\tcase \"thinking_delta\":\n\t\t\tprocess.stdout.write(event.delta);\n\t\t\tbreak;\n\t\tcase \"thinking_end\":\n\t\t\tconsole.log(\"[Thinking complete]\");\n\t\t\tbreak;\n\t\tcase \"toolcall_start\":\n\t\t\tconsole.log(`\\n[Tool call started: index ${event.contentIndex}]`);\n\t\t\tbreak;\n\t\tcase \"toolcall_delta\":\n\t\t\t// Partial tool arguments are being streamed\n\t\t\tconst partialCall = event.partial.content[event.contentIndex];\n\t\t\tif (partialCall.type === \"toolCall\") {\n\t\t\t\tconsole.log(`[Streaming args for ${partialCall.name}]`);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"toolcall_end\":\n\t\t\tconsole.log(`\\nTool called: ${event.toolCall.name}`);\n\t\t\tconsole.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`);\n\t\t\tbreak;\n\t\tcase \"done\":\n\t\t\tconsole.log(`\\nFinished: ${event.reason}`);\n\t\t\tbreak;\n\t\tcase \"error\":\n\t\t\tconsole.error(`Error: ${event.error}`);\n\t\t\tbreak;\n\t}\n}\n\n// Get the final message after streaming, add it to the context\nconst finalMessage = await s.result();\ncontext.messages.push(finalMessage);\n\n// Handle tool calls if any\nconst toolCalls = finalMessage.content.filter((b) => b.type === \"toolCall\");\nfor (const call of toolCalls) {\n\t// Execute the tool\n\tconst result =\n\t\tcall.name === \"get_time\"\n\t\t\t? new Date().toLocaleString(\"en-US\", {\n\t\t\t\t\ttimeZone: call.arguments.timezone || \"UTC\",\n\t\t\t\t\tdateStyle: \"full\",\n\t\t\t\t\ttimeStyle: \"long\",\n\t\t\t\t})\n\t\t\t: \"Unknown tool\";\n\n\t// Add tool result to context (supports text and images)\n\tcontext.messages.push({\n\t\trole: \"toolResult\",\n\t\ttoolCallId: call.id,\n\t\ttoolName: call.name,\n\t\tcontent: [{ type: \"text\", text: result }],\n\t\tisError: false,\n\t\ttimestamp: Date.now(),\n\t});\n}\n\n// Continue if there were tool calls\nif (toolCalls.length > 0) {\n\tconst continuation = await complete(model, context);\n\tcontext.messages.push(continuation);\n\tconsole.log(\"After tool execution:\", continuation.content);\n}\n\nconsole.log(`Total tokens: ${finalMessage.usage.input} in, ${finalMessage.usage.output} out`);\nconsole.log(`Cost: $${finalMessage.usage.cost.total.toFixed(4)}`);\n\n// Option 2: Get complete response without streaming\nconst response = await complete(model, context);\n\nfor (const block of response.content) {\n\tif (block.type === \"text\") {\n\t\tconsole.log(block.text);\n\t} else if (block.type === \"toolCall\") {\n\t\tconsole.log(`Tool: ${block.name}(${JSON.stringify(block.arguments)})`);\n\t}\n}\n```\n\n## Tools\n\nTools enable LLMs to interact with external systems. This library uses **Zod** schemas for type-safe tool definitions with automatic validation. Schemas are converted to JSON Schema for providers as needed.\n\n### Defining Tools\n\n```typescript\nimport { z, Tool } from \"@oh-my-pi/pi-ai\";\n\n// Define tool parameters with Zod\nconst weatherTool: Tool = {\n\tname: \"get_weather\",\n\tdescription: \"Get current weather for a location\",\n\tparameters: z.object({\n\t\tlocation: z.string().describe(\"City name or coordinates\"),\n\t\tunits: z.enum([\"celsius\", \"fahrenheit\"]).default(\"celsius\"),\n\t}),\n};\n\nconst bookMeetingTool: Tool = {\n\tname: \"book_meeting\",\n\tdescription: \"Schedule a meeting\",\n\tparameters: z.object({\n\t\ttitle: z.string().min(1),\n\t\tstartTime: z.string().describe(\"ISO 8601 date-time\"),\n\t\tendTime: z.string().describe(\"ISO 8601 date-time\"),\n\t\tattendees: z.array(z.email()).min(1),\n\t}),\n};\n```\n\n### Handling Tool Calls\n\nTool results use content blocks and can include both text and images:\n\n```typescript\nimport * as fs from \"node:fs\";\n\nconst context: Context = {\n\tmessages: [{ role: \"user\", content: \"What is the weather in London?\" }],\n\ttools: [weatherTool],\n};\n\nconst response = await complete(model, context);\n\n// Check for tool calls in the response\nfor (const block of response.content) {\n\tif (block.type === \"toolCall\") {\n\t\t// Execute your tool with the arguments\n\t\t// See \"Validating Tool Arguments\" section for validation\n\t\tconst result = await executeWeatherApi(block.arguments);\n\n\t\t// Add tool result with text content\n\t\tcontext.messages.push({\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId: block.id,\n\t\t\ttoolName: block.name,\n\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result) }],\n\t\t\tisError: false,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n}\n\n// Tool results can also include images (for vision-capable models)\nconst imageBuffer = fs.readFileSync(\"chart.png\");\ncontext.messages.push({\n\trole: \"toolResult\",\n\ttoolCallId: \"tool_xyz\",\n\ttoolName: \"generate_chart\",\n\tcontent: [\n\t\t{ type: \"text\", text: \"Generated chart showing temperature trends\" },\n\t\t{ type: \"image\", data: imageBuffer.toBase64(), mimeType: \"image/png\" },\n\t],\n\tisError: false,\n\ttimestamp: Date.now(),\n});\n```\n\n### Streaming Tool Calls with Partial JSON\n\nDuring streaming, tool call arguments are progressively parsed as they arrive. This enables real-time UI updates before the complete arguments are available:\n\n```typescript\nconst s = stream(model, context);\n\nfor await (const event of s) {\n\tif (event.type === \"toolcall_delta\") {\n\t\tconst toolCall = event.partial.content[event.contentIndex];\n\n\t\t// toolCall.arguments contains partially parsed JSON during streaming\n\t\t// This allows for progressive UI updates\n\t\tif (toolCall.type === \"toolCall\" && toolCall.arguments) {\n\t\t\t// BE DEFENSIVE: arguments may be incomplete\n\t\t\t// Example: Show file path being written even before content is complete\n\t\t\tif (toolCall.name === \"write_file\" && toolCall.arguments.path) {\n\t\t\t\tconsole.log(`Writing to: ${toolCall.arguments.path}`);\n\n\t\t\t\t// Content might be partial or missing\n\t\t\t\tif (toolCall.arguments.content) {\n\t\t\t\t\tconsole.log(`Content preview: ${toolCall.arguments.content.substring(0, 100)}...`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (event.type === \"toolcall_end\") {\n\t\t// Here toolCall.arguments is complete (but not yet validated)\n\t\tconst toolCall = event.toolCall;\n\t\tconsole.log(`Tool completed: ${toolCall.name}`, toolCall.arguments);\n\t}\n}\n```\n\n**Important notes about partial tool arguments:**\n\n- During `toolcall_delta` events, `arguments` contains the best-effort parse of partial JSON\n- Fields may be missing or incomplete - always check for existence before use\n- String values may be truncated mid-word\n- Arrays may be incomplete\n- Nested objects may be partially populated\n- At minimum, `arguments` will be an empty object `{}`, never `undefined`\n- The Google provider does not support function call streaming. Instead, you will receive a single `toolcall_delta` event with the full arguments.\n\n### Validating Tool Arguments\n\nWhen using `agentLoop`, tool arguments are automatically validated against your Zod parameter schemas before execution. If validation fails, the error is returned to the model as a tool result, allowing it to retry.\n\nWhen implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:\n\n```typescript\nimport { stream, validateToolCall, Tool } from \"@oh-my-pi/pi-ai\";\n\nconst tools: Tool[] = [weatherTool, calculatorTool];\nconst s = stream(model, { messages, tools });\n\nfor await (const event of s) {\n\tif (event.type === \"toolcall_end\") {\n\t\tconst toolCall = event.toolCall;\n\n\t\ttry {\n\t\t\t// Validate arguments against the tool's schema (throws on invalid args)\n\t\t\tconst validatedArgs = validateToolCall(tools, toolCall);\n\t\t\tconst result = await executeMyTool(toolCall.name, validatedArgs);\n\t\t\t// ... add tool result to context\n\t\t} catch (error) {\n\t\t\t// Validation failed - return error as tool result so model can retry\n\t\t\tcontext.messages.push({\n\t\t\t\trole: \"toolResult\",\n\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\ttoolName: toolCall.name,\n\t\t\t\tcontent: [{ type: \"text\", text: error.message }],\n\t\t\t\tisError: true,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t});\n\t\t}\n\t}\n}\n```\n\n### Complete Event Reference\n\nAll streaming events emitted during assistant message generation:\n\n| Event Type | Description | Key Properties |\n| ---------------- | ------------------------ | ------------------------------------------------------------------------------------------- |\n| `start` | Stream begins | `partial`: Initial assistant message structure |\n| `text_start` | Text block starts | `contentIndex`: Position in content array |\n| `text_delta` | Text chunk received | `delta`: New text, `contentIndex`: Position |\n| `text_end` | Text block complete | `content`: Full text, `contentIndex`: Position |\n| `thinking_start` | Thinking block starts | `contentIndex`: Position in content array |\n| `thinking_delta` | Thinking chunk received | `delta`: New text, `contentIndex`: Position |\n| `thinking_end` | Thinking block complete | `content`: Full thinking, `contentIndex`: Position |\n| `toolcall_start` | Tool call begins | `contentIndex`: Position in content array |\n| `toolcall_delta` | Tool arguments streaming | `delta`: JSON chunk, `partial.content[contentIndex].arguments`: Partial parsed args |\n| `toolcall_end` | Tool call complete | `toolCall`: Complete validated tool call with `id`, `name`, `arguments` |\n| `done` | Stream complete | `reason`: Stop reason (\"stop\", \"length\", \"toolUse\"), `message`: Final assistant message |\n| `error` | Error occurred | `reason`: Error type (\"error\" or \"aborted\"), `error`: AssistantMessage with partial content |\n\n## Image Input\n\nModels with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.\n\n```typescript\nimport * as fs from \"node:fs\";\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Check if model supports images\nif (model.input.includes(\"image\")) {\n\tconsole.log(\"Model supports vision\");\n}\n\nconst imageBuffer = fs.readFileSync(\"image.png\");\nconst base64Image = imageBuffer.toBase64();\n\nconst response = await complete(model, {\n\tmessages: [\n\t\t{\n\t\t\trole: \"user\",\n\t\t\tcontent: [\n\t\t\t\t{ type: \"text\", text: \"What is in this image?\" },\n\t\t\t\t{ type: \"image\", data: base64Image, mimeType: \"image/png\" },\n\t\t\t],\n\t\t},\n\t],\n});\n\n// Access the response\nfor (const block of response.content) {\n\tif (block.type === \"text\") {\n\t\tconsole.log(block.text);\n\t}\n}\n```\n\n## Thinking/Reasoning\n\nMany models support thinking/reasoning capabilities where they can show their internal thought process. You can check if a model supports reasoning via the `reasoning` property. If you pass reasoning options to a non-reasoning model, they are silently ignored.\n\n### Unified Interface (streamSimple/completeSimple)\n\n```typescript\nimport { getModel, streamSimple, completeSimple } from \"@oh-my-pi/pi-ai\";\n\n// Many models across providers support thinking/reasoning\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\n// or getModel('openai', 'gpt-5-mini');\n// or getModel('google', 'gemini-2.5-flash');\n// or getModel('xai', 'grok-code-fast-1');\n// or getModel('groq', 'openai/gpt-oss-20b');\n// or getModel('cerebras', 'gpt-oss-120b');\n// or getModel('openrouter', 'z-ai/glm-4.5v');\n\n// Check if model supports reasoning\nif (model.reasoning) {\n\tconsole.log(\"Model supports reasoning/thinking\");\n}\n\n// Use the simplified reasoning option\nconst response = await completeSimple(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Solve: 2x + 5 = 13\" }],\n\t},\n\t{\n\t\treasoning: \"medium\", // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers)\n\t}\n);\n\n// Access thinking and text blocks\nfor (const block of response.content) {\n\tif (block.type === \"thinking\") {\n\t\tconsole.log(\"Thinking:\", block.thinking);\n\t} else if (block.type === \"text\") {\n\t\tconsole.log(\"Response:\", block.text);\n\t}\n}\n```\n\n### Provider-Specific Options (stream/complete)\n\nFor fine-grained control, use the provider-specific options:\n\n```typescript\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n// OpenAI Reasoning (o1, o3, gpt-5)\nconst openaiModel = getModel(\"openai\", \"gpt-5-mini\");\nawait complete(openaiModel, context, {\n\treasoningEffort: \"medium\",\n\treasoningSummary: \"detailed\", // OpenAI Responses API only\n});\n\n// Anthropic Thinking (Claude Sonnet 4)\nconst anthropicModel = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\nawait complete(anthropicModel, context, {\n\tthinkingEnabled: true,\n\tthinkingBudgetTokens: 8192, // Optional token limit\n});\n\n// Google Gemini Thinking\nconst googleModel = getModel(\"google\", \"gemini-2.5-flash\");\nawait complete(googleModel, context, {\n\tthinking: {\n\t\tenabled: true,\n\t\tbudgetTokens: 8192, // -1 for dynamic, 0 to disable\n\t},\n});\n```\n\n### Streaming Thinking Content\n\nWhen streaming, thinking content is delivered through specific events:\n\n```typescript\nconst s = streamSimple(model, context, { reasoning: \"high\" });\n\nfor await (const event of s) {\n\tswitch (event.type) {\n\t\tcase \"thinking_start\":\n\t\t\tconsole.log(\"[Model started thinking]\");\n\t\t\tbreak;\n\t\tcase \"thinking_delta\":\n\t\t\tprocess.stdout.write(event.delta); // Stream thinking content\n\t\t\tbreak;\n\t\tcase \"thinking_end\":\n\t\t\tconsole.log(\"\\n[Thinking complete]\");\n\t\t\tbreak;\n\t}\n}\n```\n\n## Stop Reasons\n\nEvery `AssistantMessage` includes a `stopReason` field that indicates how the generation ended:\n\n- `\"stop\"` - Normal completion, the model finished its response\n- `\"length\"` - Output hit the maximum token limit\n- `\"toolUse\"` - Model is calling tools and expects tool results\n- `\"error\"` - An error occurred during generation\n- `\"aborted\"` - Request was cancelled via abort signal\n\n## Error Handling\n\nWhen a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event:\n\n```typescript\n// In streaming\nfor await (const event of stream) {\n\tif (event.type === \"error\") {\n\t\t// event.reason is either \"error\" or \"aborted\"\n\t\t// event.error is the AssistantMessage with partial content\n\t\tconsole.error(`Error (${event.reason}):`, event.error.errorMessage);\n\t\tconsole.log(\"Partial content:\", event.error.content);\n\t}\n}\n\n// The final message will have the error details\nconst message = await stream.result();\nif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\tconsole.error(\"Request failed:\", message.errorMessage);\n\t// message.content contains any partial content received before the error\n\t// message.usage contains partial token counts and costs\n}\n```\n\n### Aborting Requests\n\nThe abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:\n\n```typescript\nimport { getModel, stream } from \"@oh-my-pi/pi-ai\";\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Abort after 2 seconds\nconst signal = AbortSignal.timeout(2000);\n\nconst s = stream(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Write a long story\" }],\n\t},\n\t{\n\t\tsignal,\n\t}\n);\n\nfor await (const event of s) {\n\tif (event.type === \"text_delta\") {\n\t\tprocess.stdout.write(event.delta);\n\t} else if (event.type === \"error\") {\n\t\t// event.reason tells you if it was \"error\" or \"aborted\"\n\t\tconsole.log(`${event.reason === \"aborted\" ? \"Aborted\" : \"Error\"}:`, event.error.errorMessage);\n\t}\n}\n\n// Get results (may be partial if aborted)\nconst response = await s.result();\nif (response.stopReason === \"aborted\") {\n\tconsole.log(\"Request was aborted:\", response.errorMessage);\n\tconsole.log(\"Partial content received:\", response.content);\n\tconsole.log(\"Tokens used:\", response.usage);\n}\n```\n\n### Continuing After Abort\n\nAborted messages can be added to the conversation context and continued in subsequent requests:\n\n```typescript\nconst context = {\n\tmessages: [{ role: \"user\", content: \"Explain quantum computing in detail\" }],\n};\n\n// First request gets aborted after 2 seconds\nconst controller1 = new AbortController();\nsetTimeout(() => controller1.abort(), 2000);\n\nconst partial = await complete(model, context, { signal: controller1.signal });\n\n// Add the partial response to context\ncontext.messages.push(partial);\ncontext.messages.push({ role: \"user\", content: \"Please continue\" });\n\n// Continue the conversation\nconst continuation = await complete(model, context);\n```\n\n### Common Stream Options\n\nAll providers accept the base `StreamOptions` (in addition to provider-specific options):\n\n- `apiKey`: Override the provider API key\n- `headers`: Extra request headers merged on top of model-defined headers\n- `sessionId`: Provider-specific session identifier (prompt caching/routing)\n- `signal`: Abort in-flight requests\n- `onPayload`: Callback invoked with the provider request payload just before sending\n\nExample:\n\n```typescript\nconst response = await complete(model, context, {\n\tapiKey: \"sk-live\",\n\theaders: { \"X-Debug-Trace\": \"true\" },\n\tonPayload: (payload) => {\n\t\tconsole.log(\"request payload\", payload);\n\t},\n});\n```\n\n## APIs, Models, and Providers\n\nThe library implements 4 API interfaces, each with its own streaming function and options:\n\n- **`anthropic-messages`**: Anthropic's Messages API (`streamAnthropic`, `AnthropicOptions`)\n- **`google-generative-ai`**: Google's Generative AI API (`streamGoogle`, `GoogleOptions`)\n- **`openai-completions`**: OpenAI's Chat Completions API (`streamOpenAICompletions`, `OpenAICompletionsOptions`)\n- **`openai-responses`**: OpenAI's Responses API (`streamOpenAIResponses`, `OpenAIResponsesOptions`)\n\n### Providers and Models\n\nA **provider** offers models through a specific API. For example:\n\n- **Anthropic** models use the `anthropic-messages` API\n- **Google** models use the `google-generative-ai` API\n- **OpenAI** models use the `openai-responses` API\n- **Mistral, xAI, Cerebras, Groq, etc.** models use the `openai-completions` API (OpenAI-compatible)\n\n### Querying Providers and Models\n\n```typescript\nimport { getProviders, getModels, getModel } from \"@oh-my-pi/pi-ai\";\n\n// Get all available providers\nconst providers = getProviders();\nconsole.log(providers); // ['openai', 'anthropic', 'google', 'xai', 'groq', ...]\n\n// Get all models from a provider (fully typed)\nconst anthropicModels = getModels(\"anthropic\");\nfor (const model of anthropicModels) {\n\tconsole.log(`${model.id}: ${model.name}`);\n\tconsole.log(` API: ${model.api}`); // 'anthropic-messages'\n\tconsole.log(` Context: ${model.contextWindow} tokens`);\n\tconsole.log(` Vision: ${model.input.includes(\"image\")}`);\n\tconsole.log(` Reasoning: ${model.reasoning}`);\n}\n\n// Get a specific model (both provider and model ID are auto-completed in IDEs)\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\nconsole.log(`Using ${model.name} via ${model.api} API`);\n```\n\n### Custom Models\n\nYou can create custom models for local inference servers or custom endpoints.\n\nFor local Ollama, `OLLAMA_API_KEY` is optional and mainly needed for authenticated/self-hosted gateways. `ollama` remains the local OpenAI-compatible runtime integration.\n\n```typescript\nimport { Model, stream } from \"@oh-my-pi/pi-ai\";\n\n// Example: local Ollama using the OpenAI-compatible API\nconst ollamaModel: Model<\"openai-completions\"> = {\n\tid: \"llama-3.1-8b\",\n\tname: \"Llama 3.1 8B (Ollama)\",\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 32000,\n};\n\nconst localResponse = await stream(ollamaModel, context, {\n\tapiKey: process.env.OLLAMA_API_KEY, // Optional; local Ollama usually runs without auth\n});\n\n// Example: Ollama Cloud using the native /api/chat transport\nconst ollamaCloudModel: Model<\"ollama-chat\"> = {\n\tid: \"gpt-oss:120b\",\n\tname: \"GPT OSS 120B (Ollama Cloud)\",\n\tapi: \"ollama-chat\",\n\tprovider: \"ollama-cloud\",\n\tbaseUrl: \"https://ollama.com\",\n\treasoning: true,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 262144,\n\tmaxTokens: 8192,\n};\n\nconst cloudResponse = await stream(ollamaCloudModel, context, {\n\tapiKey: process.env.OLLAMA_CLOUD_API_KEY,\n});\n\n// Example: LiteLLM proxy with explicit compat settings\nconst litellmModel: Model<\"openai-completions\"> = {\n\tid: \"gpt-4o\",\n\tname: \"GPT-4o (via LiteLLM)\",\n\tapi: \"openai-completions\",\n\tprovider: \"litellm\",\n\tbaseUrl: \"http://localhost:4000/v1\",\n\treasoning: false,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 16384,\n\tcompat: {\n\t\tsupportsStore: false, // LiteLLM doesn't support the store field\n\t},\n};\n\n// Example: Custom endpoint with headers (bypassing Cloudflare bot detection)\nconst proxyModel: Model<\"anthropic-messages\"> = {\n\tid: \"claude-sonnet-4\",\n\tname: \"Claude Sonnet 4 (Proxied)\",\n\tapi: \"anthropic-messages\",\n\tprovider: \"custom-proxy\",\n\tbaseUrl: \"https://proxy.example.com/v1\",\n\treasoning: true,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },\n\tcontextWindow: 200000,\n\tmaxTokens: 8192,\n\theaders: {\n\t\t\"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36\",\n\t\t\"X-Custom-Auth\": \"bearer-token-here\",\n\t},\n};\n```\n\n### OpenAI Compatibility Settings\n\nThe `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for known providers (Cerebras, xAI, Mistral, Chutes, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field:\n\n```typescript\ninterface OpenAICompat {\n\tsupportsStore?: boolean; // Whether provider supports the `store` field (default: true)\n\tsupportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)\n\tsupportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)\n\tmaxTokensField?: \"max_completion_tokens\" | \"max_tokens\"; // Which field name to use (default: max_completion_tokens)\n\textraBody?: Record<string, unknown>; // Extra request-body fields for custom proxy routing or provider-specific options\n}\n```\n\nIf `compat` is not set, the library falls back to URL-based detection. If `compat` is partially set, unspecified fields use the detected defaults. This is useful for:\n\n- **LiteLLM proxies**: May not support `store` field\n- **Custom inference servers**: May use non-standard field names\n- **Self-hosted endpoints**: May have different feature support\n\n### Type Safety\n\nModels are typed by their API, ensuring type-safe options:\n\n```typescript\n// TypeScript knows this is an Anthropic model\nconst claude = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\n\n// So these options are type-checked for AnthropicOptions\nawait stream(claude, context, {\n\tthinkingEnabled: true, // ✓ Valid for anthropic-messages\n\tthinkingBudgetTokens: 2048, // ✓ Valid for anthropic-messages\n\t// reasoningEffort: 'high' // ✗ TypeScript error: not valid for anthropic-messages\n});\n```\n\n## Cross-Provider Handoffs\n\nThe library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including thinking blocks, tool calls, and tool results.\n\n### How It Works\n\nWhen messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:\n\n- **User and tool result messages** are passed through unchanged\n- **Assistant messages from the same provider/API** are preserved as-is\n- **Assistant messages from different providers** have their thinking blocks converted to text with `<thinking>` tags\n- **Tool calls and regular text** are preserved unchanged\n\n### Example: Multi-Provider Conversation\n\n```typescript\nimport { getModel, complete, Context } from \"@oh-my-pi/pi-ai\";\n\n// Start with Claude\nconst claude = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\nconst context: Context = {\n\tmessages: [],\n};\n\ncontext.messages.push({ role: \"user\", content: \"What is 25 * 18?\" });\nconst claudeResponse = await complete(claude, context, {\n\tthinkingEnabled: true,\n});\ncontext.messages.push(claudeResponse);\n\n// Switch to GPT-5 - it will see Claude's thinking as <thinking> tagged text\nconst gpt5 = getModel(\"openai\", \"gpt-5-mini\");\ncontext.messages.push({ role: \"user\", content: \"Is that calculation correct?\" });\nconst gptResponse = await complete(gpt5, context);\ncontext.messages.push(gptResponse);\n\n// Switch to Gemini\nconst gemini = getModel(\"google\", \"gemini-2.5-flash\");\ncontext.messages.push({ role: \"user\", content: \"What was the original question?\" });\nconst geminiResponse = await complete(gemini, context);\n```\n\n### Provider Compatibility\n\nAll providers can handle messages from other providers, including:\n\n- Text content\n- Tool calls and tool results (including images in tool results)\n- Thinking/reasoning blocks (transformed to tagged text for cross-provider compatibility)\n- Aborted messages with partial content\n\nThis enables flexible workflows where you can:\n\n- Start with a fast model for initial responses\n- Switch to a more capable model for complex reasoning\n- Use specialized models for specific tasks\n- Maintain conversation continuity across provider outages\n\n## Context Serialization\n\nThe `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:\n\n```typescript\nimport { Context, getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n// Create and use a context\nconst context: Context = {\n\tsystemPrompt: [\"You are a helpful assistant.\"],\n\tmessages: [{ role: \"user\", content: \"What is TypeScript?\" }],\n};\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\nconst response = await complete(model, context);\ncontext.messages.push(response);\n\n// Serialize the entire context\nconst serialized = JSON.stringify(context);\nconsole.log(\"Serialized context size:\", serialized.length, \"bytes\");\n\n// Save to database, localStorage, file, etc.\nlocalStorage.setItem(\"conversation\", serialized);\n\n// Later: deserialize and continue the conversation\nconst restored: Context = JSON.parse(localStorage.getItem(\"conversation\")!);\nrestored.messages.push({ role: \"user\", content: \"Tell me more about its type system\" });\n\n// Continue with any model\nconst newModel = getModel(\"anthropic\", \"claude-haiku-4-5-20251001\");\nconst continuation = await complete(newModel, restored);\n```\n\n> **Note**: If the context contains images (encoded as base64 as shown in the Image Input section), those will also be serialized.\n\n## Browser Usage\n\nThe library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers:\n\n```typescript\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n// API key must be passed explicitly in browser\nconst model = getModel(\"anthropic\", \"claude-haiku-4-5-20251001\");\n\nconst response = await complete(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Hello!\" }],\n\t},\n\t{\n\t\tapiKey: \"your-api-key\",\n\t}\n);\n```\n\n> **Security Warning**: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys. Only use this approach for internal tools or demos. For production applications, use a backend proxy that keeps your API keys secure.\n\n### Environment Variables (Node.js only)\n\nIn Node.js environments, you can set environment variables to avoid passing API keys:\n\n| Provider | Environment Variable(s) |\n| -------------- | ---------------------------------------------------------------------------- |\n| OpenAI | `OPENAI_API_KEY` |\n| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` (or `ANTHROPIC_FOUNDRY_API_KEY` when `CLAUDE_CODE_USE_FOUNDRY=true`) |\n| Google | `GEMINI_API_KEY` |\n| Vertex AI | `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |\n| Mistral | `MISTRAL_API_KEY` |\n| Groq | `GROQ_API_KEY` |\n| Cerebras | `CEREBRAS_API_KEY` |\n| Together | `TOGETHER_API_KEY` |\n| Qianfan | `QIANFAN_API_KEY` |\n| Hugging Face | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` |\n| Synthetic | `SYNTHETIC_API_KEY` |\n| NVIDIA | `NVIDIA_API_KEY` |\n| NanoGPT | `NANO_GPT_API_KEY` |\n| Novita | `NOVITA_API_KEY` |\n| Venice | `VENICE_API_KEY` |\n| Moonshot | `MOONSHOT_API_KEY` |\n| xAI | `XAI_API_KEY` |\n| OpenRouter | `OPENROUTER_API_KEY` |\n| LiteLLM | `LITELLM_API_KEY` |\n| Ollama | `OLLAMA_API_KEY` (optional for local deployments) |\n| Ollama Cloud | `OLLAMA_CLOUD_API_KEY` |\n| Qwen Portal | `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY` |\n| QwenCloud Token Plan | `ALIBABA_TOKEN_PLAN_API_KEY` or `BAILIAN_TOKEN_PLAN_API_KEY` |\n| zAI | `ZAI_API_KEY` |\n| Umans AI Coding Plan | `UMANS_AI_CODING_PLAN_API_KEY` |\n| MiniMax Code | `MINIMAX_CODE_API_KEY` (international) or `MINIMAX_CODE_CN_API_KEY` (China) |\n| Xiaomi MiMo | `XIAOMI_API_KEY` |\n| ZenMux | `ZENMUX_API_KEY` |\n| vLLM | `VLLM_API_KEY` |\n| Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY` |\n| GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |\n\nFor Cloudflare AI Gateway models, use provider base URL format\n`https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`.\n\nFor Anthropic Foundry routing, set `CLAUDE_CODE_USE_FOUNDRY=true` plus:\n`FOUNDRY_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, optional `ANTHROPIC_CUSTOM_HEADERS`,\nand optional mTLS material (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`).\n\n`NODE_EXTRA_CA_CERTS` (PEM file path or inline PEM, mirroring Node's contract)\nis honoured on every provider fetch — OpenAI-compatible, Codex, Ollama, Azure\nResponses, Google, and Anthropic alike — for corporate relays or private CA\nbundles. Bun's `fetch` does not consume the env var natively, so omp injects\nthe bundle into `RequestInit.tls.ca` and seeds the system root store\nalongside it.\n\nProvider endpoint defaults for the current OpenAI-compatible integrations:\n\n- Together: `https://api.together.xyz/v1`\n- Moonshot: `https://api.moonshot.ai/v1`\n- Qianfan: `https://qianfan.baidubce.com/v2`\n- NVIDIA: `https://integrate.api.nvidia.com/v1`\n- NanoGPT: `https://nano-gpt.com/api/v1`\n- Novita: `https://api.novita.ai/openai/v1`\n- Hugging Face Inference: `https://router.huggingface.co/v1`\n- Venice: `https://api.venice.ai/api/v1`\n- Xiaomi MiMo: `https://api.xiaomimimo.com/anthropic`\n- ZenMux (OpenAI): `https://zenmux.ai/api/v1`\n- ZenMux (Anthropic models): `https://zenmux.ai/api/anthropic`\n- Umans AI Coding Plan: `https://api.code.umans.ai`\n- vLLM: `http://127.0.0.1:8000/v1`\n- Ollama: local OpenAI-compatible runtime (`http://127.0.0.1:11434/v1`)\n- Ollama Cloud: native Ollama API host (`https://ollama.com/api`, configured here as base URL `https://ollama.com`)\n- LiteLLM: `http://localhost:4000/v1`\n- Cloudflare AI Gateway: `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`\n- Qwen Portal: `https://portal.qwen.ai/v1`\nWhen set, the library automatically uses these keys:\n\n```typescript\n// Uses OPENAI_API_KEY from environment\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\nconst response = await complete(model, context);\n\n// Or override with explicit key\nconst response = await complete(model, context, {\n\tapiKey: \"sk-different-key\",\n});\n```\n\n### Checking Environment Variables\n\n```typescript\nimport { getEnvApiKey } from \"@oh-my-pi/pi-ai\";\n\n// Check if an API key is set in environment variables\nconst key = getEnvApiKey(\"openai\"); // checks OPENAI_API_KEY\n```\n\n## OAuth Providers\n\nSeveral providers support OAuth authentication (some also support static API keys):\n\n- **Anthropic** (Claude Pro/Max subscription)\n- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)\n- **GitHub Copilot** (Copilot subscription)\n- **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)\n- **Antigravity** (Free Gemini 3, Claude, GPT-OSS via Google Cloud)\n- **Qwen Portal** (Qwen OAuth token or API key)\n\nFor paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID.\n\n### Vertex AI (ADC)\n\nVertex AI models use Application Default Credentials (ADC):\n\n- **Local development**: Run `gcloud auth application-default login`\n- **CI/Production**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to a service account JSON key file\n\nAlso set `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) and `GOOGLE_CLOUD_LOCATION`. You can also pass `project`/`location` in the call options.\n\nExample:\n\n```bash\n# Local (uses your user credentials)\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=\"my-project\"\nexport GOOGLE_CLOUD_LOCATION=\"us-central1\"\n\n# CI/Production (service account key file)\nexport GOOGLE_APPLICATION_CREDENTIALS=\"/path/to/service-account.json\"\n```\n\n```typescript\nimport { getModel, complete } from \"@oh-my-pi/pi-ai\";\n\n(async () => {\n\tconst model = getModel(\"google-vertex\", \"gemini-2.5-flash\");\n\tconst response = await complete(model, {\n\t\tmessages: [{ role: \"user\", content: \"Hello from Vertex AI\" }],\n\t});\n\n\tfor (const block of response.content) {\n\t\tif (block.type === \"text\") console.log(block.text);\n\t}\n})().catch(console.error);\n```\n\nOfficial docs: [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials)\n\n### CLI Login\n\nAuthenticate via the [`omp`](https://omp.sh) coding-agent CLI, which drives this library's OAuth/API-key flows in-process and persists into `agent.db`:\n\n```bash\nomp auth-broker login # interactive provider selection\nomp auth-broker login anthropic # login to a specific provider\nomp auth-broker login vllm # store vLLM API key (or placeholder for local no-auth)\nomp auth-broker list # list supported providers\nomp auth-broker logout # interactive — pick a stored credential to remove\n```\n\nCredentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.\n\n`login` supports OAuth providers (Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.\n\nFor the current API-key onboarding flows, the library covers Together, Moonshot, Qianfan, NVIDIA, NanoGPT, Novita, Hugging Face, Venice, Xiaomi, vLLM, LiteLLM, Cloudflare AI Gateway, Qwen Portal, and Ollama Cloud. Ollama remains the local runtime integration; set `OLLAMA_API_KEY` only when your local or self-hosted deployment enforces bearer auth.\n\n### Programmatic OAuth\n\nThe library provides login and token refresh functions. Credential storage is the caller's responsibility.\n\n```typescript\nimport {\n\t// Login functions (return credentials, do not store)\n\tloginAnthropic,\n\tloginOpenAICodex,\n\tloginGitHubCopilot,\n\tloginGeminiCli,\n\tloginAntigravity,\n\tloginCloudflareAiGateway,\n\tloginHuggingface,\n\tloginLiteLLM,\n\tloginMoonshot,\n\tloginNvidia,\n\tloginNanoGPT,\n\tloginQianfan,\n\tloginQwenPortal,\n\tloginTogether,\n\tloginVenice,\n\tloginVllm,\n\tloginXiaomi,\n\n\t// Token management\n\trefreshOAuthToken, // (provider, credentials) => new credentials\n\tgetOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null\n\n\t// Types\n\ttype OAuthProvider, // includes 'anthropic', 'openai-codex', 'github-copilot', 'google-gemini-cli', 'google-antigravity', 'together', 'moonshot', 'qianfan', 'nvidia', 'nanogpt', 'novita', 'huggingface', 'venice', 'xiaomi', 'vllm', 'litellm', 'cloudflare-ai-gateway', 'qwen-portal', ...\n\ttype OAuthCredentials,\n} from \"@oh-my-pi/pi-ai\";\n```\n\n`loginOpenAICodex` accepts an optional `originator` value used in the OAuth flow:\n\n```typescript\nawait loginOpenAICodex({\n\tonAuth: ({ url }) => console.log(url),\n\toriginator: \"my-cli\",\n});\n```\n\n### Login Flow Example\n\n```typescript\nimport { loginGitHubCopilot } from \"@oh-my-pi/pi-ai\";\nimport * as fs from \"node:fs\";\n\nconst credentials = await loginGitHubCopilot({\n\tonAuth: (url, instructions) => {\n\t\tconsole.log(`Open: ${url}`);\n\t\tif (instructions) console.log(instructions);\n\t},\n\tonPrompt: async (prompt) => {\n\t\treturn await getUserInput(prompt.message);\n\t},\n\tonProgress: (message) => console.log(message),\n});\n\n// Store credentials yourself\nconst auth = { \"github-copilot\": { type: \"oauth\", ...credentials } };\nfs.writeFileSync(\"credentials.json\", JSON.stringify(auth, null, 2));\n```\n\n### Using OAuth Tokens\n\nUse `getOAuthApiKey()` to get an API key, automatically refreshing if expired:\n\n```typescript\nimport { getModel, complete, getOAuthApiKey } from \"@oh-my-pi/pi-ai\";\nimport * as fs from \"node:fs\";\n\n// Load your stored credentials\nconst auth = JSON.parse(fs.readFileSync(\"credentials.json\", \"utf-8\"));\n\n// Get API key (refreshes if expired)\nconst result = await getOAuthApiKey(\"github-copilot\", auth);\nif (!result) throw new Error(\"Not logged in\");\n\n// Save refreshed credentials\nauth[\"github-copilot\"] = { type: \"oauth\", ...result.newCredentials };\nfs.writeFileSync(\"credentials.json\", JSON.stringify(auth, null, 2));\n\n// Use the API key\nconst model = getModel(\"github-copilot\", \"gpt-4o\");\nconst response = await complete(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Hello!\" }],\n\t},\n\t{ apiKey: result.apiKey }\n);\n```\n\n### Provider Notes\n\n**OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options.\n\n**GitHub Copilot**: If you get \"The requested model is not supported\" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click \"Enable\".\n\n**Google Gemini CLI / Antigravity**: These use Google Cloud OAuth. The `apiKey` returned by `getOAuthApiKey()` is a JSON string containing both the token and project ID, which the library handles automatically.\n\n## License\n\nMIT\n"}}, "packages/catalog/src/compat/openai.ts": {"5039b33a11b4a5cb0b74e7a3080d06ef5daa0813": {"t": 1785746049.0198019, "n": 37996, "h": "2e16d152b7fd7176", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/** Hosts that accept only none/auto/required rather than a named tool-choice object. */\nconst STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS: Record<string, true> = {\n\t\"llama.cpp\": true,\n\t\"lm-studio\": true,\n};\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDeepseekReasoning = isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekReasoning;\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\t// Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)\n\t// implements a subset of chat-completions and 400s on `store` (\"Unknown name \\\"store\\\"\").\n\tconst isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, \"googleAistudio\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\tisGoogleAistudioOpenAI ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\t// DeepSeek reasoning models on OpenCode Zen/Go 400 with\n\t\t// \"Thinking mode does not support this tool_choice\" when a specific\n\t\t// function is forced while the gateway's default thinking mode is active.\n\t\t// Downgrade only on those gateways: other hosts can turn thinking off via\n\t\t// disableReasoningOnToolChoice and must retain hard tool selection.\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking && !(isOpenCodeHost && isDeepseekReasoning),\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[provider] !== true,\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "93fe2ca9a9836ce32953de1799d8d74734cbccb9": {"t": 1785759193.064984, "n": 37996, "h": "2e16d152b7fd7176", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/** Hosts that accept only none/auto/required rather than a named tool-choice object. */\nconst STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS: Record<string, true> = {\n\t\"llama.cpp\": true,\n\t\"lm-studio\": true,\n};\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDeepseekReasoning = isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekReasoning;\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\t// Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)\n\t// implements a subset of chat-completions and 400s on `store` (\"Unknown name \\\"store\\\"\").\n\tconst isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, \"googleAistudio\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\tisGoogleAistudioOpenAI ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\t// DeepSeek reasoning models on OpenCode Zen/Go 400 with\n\t\t// \"Thinking mode does not support this tool_choice\" when a specific\n\t\t// function is forced while the gateway's default thinking mode is active.\n\t\t// Downgrade only on those gateways: other hosts can turn thinking off via\n\t\t// disableReasoningOnToolChoice and must retain hard tool selection.\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking && !(isOpenCodeHost && isDeepseekReasoning),\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[provider] !== true,\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "879d8276c1c179f503d9c4b97996b3e329637162": {"t": 1785759194.5472858, "n": 38000, "h": "248b918d6ee007f7", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/** Hosts that accept only none/auto/required rather than a named tool-choice object. */\nconst STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS: Record<string, true> = {\n\t\"llama.cpp\": true,\n\t\"lm-studio\": true,\n};\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDeepseekReasoning = isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekReasoning;\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\t// Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)\n\t// implements a subset of chat-completions and 400s on `store` (\"Unknown name \\\"store\\\"\").\n\tconst isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, \"googleAistudio\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\tisGoogleAistudioOpenAI ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\t// DeepSeek reasoning models on OpenCode Zen/Go 400 with\n\t\t// \"Thinking mode does not support this tool_choice\" when a specific\n\t\t// function is forced while the gateway's default thinking mode is active.\n\t\t// Downgrade only on those gateways: other hosts can turn thinking off via\n\t\t// disableReasoningOnToolChoice and must retain hard tool selection.\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking && !(isOpenCodeProvider && isDeepseekReasoning),\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[provider] !== true,\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "114a10d340406bfb04cb614c164a1d2436b7edfe": {"t": 1785759196.9028552, "n": 38140, "h": "549f7ac2077b4b9a", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/** Hosts that accept only none/auto/required rather than a named tool-choice object. */\nconst STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS: Record<string, true> = {\n\t\"llama.cpp\": true,\n\t\"lm-studio\": true,\n};\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDeepseekReasoning = isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekReasoning;\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\t// Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)\n\t// implements a subset of chat-completions and 400s on `store` (\"Unknown name \\\"store\\\"\").\n\tconst isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, \"googleAistudio\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\tisGoogleAistudioOpenAI ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\t// DeepSeek reasoning models (deepseek-reasoner, DeepSeek V4 thinking) 400\n\t\t// with \"Thinking mode does not support this tool_choice\" when a specific\n\t\t// function is forced while thinking is active. Direct-API DeepSeek drops\n\t\t// tool_choice entirely (supportsToolChoice above); gateway-hosted DeepSeek\n\t\t// (OpenCode Zen/Go, etc.) keeps tools available but must downgrade a forced\n\t\t// selector to `auto`, since dropping `reasoning_effort` alone does not turn\n\t\t// the gateway's default thinking mode off (#7315).\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking && !isDeepseekReasoning,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[provider] !== true,\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "2429efab869865218a465ba93ded436516acaa32": {"t": 1785759198.3276925, "n": 37547, "h": "9742c941aa42e5c8", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/** Hosts that accept only none/auto/required rather than a named tool-choice object. */\nconst STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS: Record<string, true> = {\n\t\"llama.cpp\": true,\n\t\"lm-studio\": true,\n};\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\t// Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)\n\t// implements a subset of chat-completions and 400s on `store` (\"Unknown name \\\"store\\\"\").\n\tconst isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, \"googleAistudio\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\tisGoogleAistudioOpenAI ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[provider] !== true,\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "10b5dcbca568ee31784479162a5956dab3af591e": {"t": 1785759199.8953285, "n": 37258, "h": "781b5be62e2f5952", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/** Hosts that accept only none/auto/required rather than a named tool-choice object. */\nconst STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS: Record<string, true> = {\n\t\"llama.cpp\": true,\n\t\"lm-studio\": true,\n};\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[provider] !== true,\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "ef936470d3495202060eaac369df7235e05cf294": {"t": 1785759201.549912, "n": 36988, "h": "d139e54b3594e4cb", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\" && provider !== \"lm-studio\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "a421ea8acdc79216edd4e1b2baf7a32e00d56ba1": {"t": 1785759203.3321514, "n": 36960, "h": "98349fc174063f11", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "c12f6206b972bb8adf40741347f18d55b2828b31": {"t": 1785759204.8969111, "n": 36795, "h": "7bb8c9932cf66339", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "8906595c497ff6c8a1a825ba11a8285be88c83de": {"t": 1785759206.4768188, "n": 36569, "h": "fb6abc50fa1c9315", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport { bareModelId, parseOpenAIModel, semverGte } from \"../identity/classify\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\tif (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;\n\tconst model = parseOpenAIModel(bareModelId(modelId));\n\treturn model !== null && semverGte(model.version, \"5.6\");\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "ed0f5d6e2948cd27bbbb309a7f43058ba11f281e": {"t": 1785759208.1153286, "n": 35728, "h": "070e6c18e9f5e136", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, \"vercelAIGateway\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t\tisVercelGatewayHost: compat.isVercelGatewayHost,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "7ae6ebfa8b3db854ee9b52960b1e7123f10fe8ef": {"t": 1785759209.8170288, "n": 36402, "h": "bc21948b98548c70", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep\n * this intentionally narrow: compatible gateways and older OpenAI models\n * reject the new request fields unless their catalog compat opts in.\n */\nfunction supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {\n\treturn isOfficialOpenAIEndpoint(provider, baseUrl) && /^gpt-5\\.6(?:[-.]|$)/i.test(modelId);\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\tsupportsPromptCacheBreakpoints,\n\t\tpromptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? \"30m\" : undefined,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "401fca701ba4b2779a128db4be4ec773d8dd2327": {"t": 1785759211.5962512, "n": 35502, "h": "dd578313835dfa02", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "1acda6c1e614907f88ddea10bd3f79ed8bc947c7": {"t": 1785759213.1213038, "n": 34588, "h": "d4980e9d37f9f5aa", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor:\n\t\t\tisMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "c2b241401b1b5a6b7872139ec9d300ec1d64a66d": {"t": 1785759215.0209963, "n": 34585, "h": "fd33630acde0d2cf", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor: isMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "cc9fba751371fe0fda183fa8f02af381bb2f9eb4": {"t": 1785759216.5173259, "n": 34544, "h": "a446d9f7546236e5", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\tmedium: \"high\",\n\txhigh: \"max\",\n\tmax: \"max\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeModelReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tmodelId: string,\n\tisMimoReasoningEffortModel: boolean,\n): void {\n\tlet detected: NonNullable<OpenAICompat[\"reasoningEffortMap\"]>;\n\tif (isKimiK3ModelId(modelId)) {\n\t\tdetected = KIMI_K3_REASONING_EFFORT_MAP;\n\t} else if (isMimoReasoningEffortModel) {\n\t\tdetected = MIMO_REASONING_EFFORT_MAP;\n\t} else {\n\t\treturn;\n\t}\n\tcompat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory\n\t// low/high/max thinking, not the K2.x binary `thinking: { type }` block.\n\tconst isKimiK3 = isKimiK3ModelId(spec.id);\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons through `reasoning_effort` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would leave K3 in an unsupported mode.\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by\n\t\t// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.\n\t\t// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning\n\t\t// via OpenAI-style `reasoning_effort`.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor: isMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "f60c7791c191f17aa949e17c9e89112e788b07d1": {"t": 1785759218.3153322, "n": 34516, "h": "eb52c9081962ebba", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Kimi K3 (native) always reasons via OpenAI-style `reasoning_effort: \"max\"`\n\t// and does NOT accept the K2.x binary `thinking: { type }` block, so it must\n\t// stay on the \"openai\" thinking dialect even though it is a Moonshot-native\n\t// Kimi model (#5756).\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3ModelId(spec.id);\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons via `reasoning_effort: \"max\"` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would strip the mandatory `max` from\n\t\t// normal forced-tool turns (e.g. plan-mode `toolChoice: \"required\"`) and\n\t\t// leave K3 in an unsupported mode (#5758 review).\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\t// Kimi-family ids trigger MFJS on any host, not just native base URLs:\n\t\t// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`\n\t\t// to Moonshot verbatim, which 400s on non-MFJS constructs.\n\t\ttoolSchemaFlavor: isMoonshotNative || isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\t// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still\n\t\t// lands on Moonshot's MFJS validator.\n\t\ttoolSchemaFlavor: isKimiModel ? \"moonshot-mfjs\" : undefined,\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "2eb35e300fe949e675861c26f7a3f589573214fc": {"t": 1785759219.8356395, "n": 34147, "h": "a7ff21842e5ea526", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Kimi K3 (native) always reasons via OpenAI-style `reasoning_effort: \"max\"`\n\t// and does NOT accept the K2.x binary `thinking: { type }` block, so it must\n\t// stay on the \"openai\" thinking dialect even though it is a Moonshot-native\n\t// Kimi model (#5756).\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3ModelId(spec.id);\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons via `reasoning_effort: \"max\"` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would strip the mandatory `max` from\n\t\t// normal forced-tool turns (e.g. plan-mode `toolChoice: \"required\"`) and\n\t\t// leave K3 in an unsupported mode (#5758 review).\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : isLocalOpenAICompatBackend ? \"grammar\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "913ec0baaef808d1713d95520694d48ae028e5e5": {"t": 1785759221.5880694, "n": 34106, "h": "abecf17d3401e512", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\tcase \"kimi\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Kimi K3 (native) always reasons via OpenAI-style `reasoning_effort: \"max\"`\n\t// and does NOT accept the K2.x binary `thinking: { type }` block, so it must\n\t// stay on the \"openai\" thinking dialect even though it is a Moonshot-native\n\t// Kimi model (#5756).\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3ModelId(spec.id);\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons via `reasoning_effort: \"max\"` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would strip the mandatory `max` from\n\t\t// normal forced-tool turns (e.g. plan-mode `toolChoice: \"required\"`) and\n\t\t// leave K3 in an unsupported mode (#5758 review).\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\tkimiApiFormat: undefined,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "fdb5d8bd0ebe36b4700c309684121fe45dc1241f": {"t": 1785759225.370116, "n": 34063, "h": "3d8294431f916561", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Kimi K3 (native) always reasons via OpenAI-style `reasoning_effort: \"max\"`\n\t// and does NOT accept the K2.x binary `thinking: { type }` block, so it must\n\t// stay on the \"openai\" thinking dialect even though it is a Moonshot-native\n\t// Kimi model (#5756).\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3ModelId(spec.id);\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\t// Native Kimi K3 always reasons via `reasoning_effort: \"max\"` (never the\n\t\t// K2.x binary `thinking` block that #827's forced-tool-choice conflict is\n\t\t// about), so suppressing its effort would strip the mandatory `max` from\n\t\t// normal forced-tool turns (e.g. plan-mode `toolChoice: \"required\"`) and\n\t\t// leave K3 in an unsupported mode (#5758 review).\n\t\tdisableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "c98b1b83d3ed8663eae7ae590503d3f6bb1938aa": {"t": 1785759228.852091, "n": 33682, "h": "d486030aafc638aa", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK3ModelId,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\t// Kimi K3 (native) always reasons via OpenAI-style `reasoning_effort: \"max\"`\n\t// and does NOT accept the K2.x binary `thinking: { type }` block, so it must\n\t// stay on the \"openai\" thinking dialect even though it is a Moonshot-native\n\t// Kimi model (#5756).\n\tconst isMoonshotKimiK3 = isMoonshotKimi && isKimiK3ModelId(spec.id);\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning &&\n\t\t\t\t\t\t\t(isKimiK26ModelId(spec.id) ||\n\t\t\t\t\t\t\t\tisMoonshotKimiK3 ||\n\t\t\t\t\t\t\t\t(isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\t(isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "b68c7245f7cc8d09052222fa0e7193a2cbf40fa3": {"t": 1785759230.5757303, "n": 33268, "h": "c6bf966292910c82", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && (isKimiK26ModelId(spec.id) || (isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "b865e6a4d7e72281f06266a930c58c9700133f5f": {"t": 1785759232.3128889, "n": 33250, "h": "1b8da3ddba9a95d9", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && (isKimiK26ModelId(spec.id) || (isMoonshotKimi && matchesKimiK27CodeFamily(spec)))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "9c78b7e7b05d337bfcee96cfd4a8d25ded98f1cf": {"t": 1785759234.166567, "n": 33230, "h": "5eb3c9aa35f103f3", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && (isKimiK26ModelId(spec.id) || matchesKimiK27CodeFamily(spec))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "f6de3735069d2d00fdcfb20b6733e3a421c7b83b": {"t": 1785759235.7042594, "n": 33181, "h": "e521d916a3dafe6e", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisOpenAISamplingRestrictedModelId,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 can spend several minutes reasoning before the first visible token. */\nconst KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && isKimiK26ModelId(spec.id)\n\t\t\t\t\t\t? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\t// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit\n\t\t// temperature/top_p/… with a 400 on every serving host (#5606).\n\t\tsupportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "0069a84c9fd66822306adff05f64d7a05b25dcda": {"t": 1785759237.5311077, "n": 34595, "h": "034955510ff9a59d", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 can spend several minutes reasoning before the first visible token. */\nconst KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n/**\n * Ollama's OpenAI-compatible `reasoning.effort` only accepts\n * `high|medium|low|max|none`; OMP's `minimal`/`xhigh` levels make the server\n * reject the turn with HTTP 400 `invalid reasoning value`. Map the two\n * unsupported levels onto the closest accepted ones. Stamped in the compat\n * builder (not only at discovery) so stale-cached and custom `ollama`-provider\n * specs are backfilled on every `buildModel`, not just on a fresh\n * `omp models refresh`. Custom OpenAI-compatible providers pointed at a local\n * Ollama port under a different provider id are not covered — they must set\n * `compat.reasoningEffortMap` themselves.\n */\nconst OLLAMA_REASONING_EFFORT_MAP: ResolvedOpenAISharedCompat[\"reasoningEffortMap\"] = { minimal: \"low\", xhigh: \"max\" };\n\n/**\n * Merge the Ollama default effort map under any explicit overrides (overrides\n * win). No-op off the local `ollama` provider or for non-reasoning models.\n */\nfunction mergeOllamaReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tprovider: string,\n\treasoning: boolean | undefined,\n): void {\n\tif (provider !== \"ollama\" || !reasoning) return;\n\tcompat.reasoningEffortMap = { ...OLLAMA_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\t// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING\n\t// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:\n\t// widening the first-event/idle abort ceiling only helps a slow local\n\t// upstream and never pushes an extra wire field, so the proxy carve-out (a\n\t// `replayReasoningContent` safety measure) must not also strip the timeout\n\t// floor. Without this, a loopback litellm fronting a cold/reprocessing\n\t// llama-server aborts prefill at the 100s default and retry-loops (#4786).\n\tconst isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && isKimiK26ModelId(spec.id)\n\t\t\t\t\t\t? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalServingBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeOllamaReasoningEffortMap(compat, provider, spec.reasoning);\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeOllamaReasoningEffortMap(variant, provider, spec.reasoning);\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\t// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is\n\t// irrelevant here; the stream-timeout floor still applies to ANY loopback /\n\t// RFC1918 backend, including local proxies (litellm), so a slow local\n\t// upstream is not aborted at the 100s default and retry-looped (#4786).\n\tconst isLocalServingBackend =\n\t\t(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||\n\t\thasLocalLoopbackBaseUrl(baseUrl);\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot's Responses endpoint rejects the `detail: \"original\"`\n\t\t// image hint with a 400; every other host preserves native-resolution\n\t\t// frames (snapcompact relies on `original`). Detect Copilot by provider id\n\t\t// or base-URL host (mirroring the Anthropic compat builder) so a model\n\t\t// pointed at the Copilot host under a different provider id still clamps.\n\t\tsupportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalServingBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeOllamaReasoningEffortMap(compat, spec.provider, spec.reasoning);\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "29c8cae9ba5c1ad2064dbc7aa964e92055dcfea6": {"t": 1785759239.3144493, "n": 33730, "h": "bc83dab771917333", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */\nconst KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n/**\n * Ollama's OpenAI-compatible `reasoning.effort` only accepts\n * `high|medium|low|max|none`; OMP's `minimal`/`xhigh` levels make the server\n * reject the turn with HTTP 400 `invalid reasoning value`. Map the two\n * unsupported levels onto the closest accepted ones. Stamped in the compat\n * builder (not only at discovery) so stale-cached and custom `ollama`-provider\n * specs are backfilled on every `buildModel`, not just on a fresh\n * `omp models refresh`. Custom OpenAI-compatible providers pointed at a local\n * Ollama port under a different provider id are not covered — they must set\n * `compat.reasoningEffortMap` themselves.\n */\nconst OLLAMA_REASONING_EFFORT_MAP: ResolvedOpenAISharedCompat[\"reasoningEffortMap\"] = { minimal: \"low\", xhigh: \"max\" };\n\n/**\n * Merge the Ollama default effort map under any explicit overrides (overrides\n * win). No-op off the local `ollama` provider or for non-reasoning models.\n */\nfunction mergeOllamaReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tprovider: string,\n\treasoning: boolean | undefined,\n): void {\n\tif (provider !== \"ollama\" || !reasoning) return;\n\tcompat.reasoningEffortMap = { ...OLLAMA_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && (isKimiK26ModelId(spec.id) || matchesKimiK27CodeFamily(spec))\n\t\t\t\t\t\t? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeOllamaReasoningEffortMap(compat, provider, spec.reasoning);\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeOllamaReasoningEffortMap(variant, provider, spec.reasoning);\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot's Responses endpoint rejects the `detail: \"original\"`\n\t\t// image hint with a 400; every other host preserves native-resolution\n\t\t// frames (snapcompact relies on `original`). Detect Copilot by provider id\n\t\t// or base-URL host (mirroring the Anthropic compat builder) so a model\n\t\t// pointed at the Copilot host under a different provider id still clamps.\n\t\tsupportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeOllamaReasoningEffortMap(compat, spec.provider, spec.reasoning);\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "7cdecd824a871efce161fe68aa9e230ca8a8788b": {"t": 1785759240.8158028, "n": 33699, "h": "01bc90b9db5742ca", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 can spend several minutes reasoning before the first visible token. */\nconst KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n/**\n * Ollama's OpenAI-compatible `reasoning.effort` only accepts\n * `high|medium|low|max|none`; OMP's `minimal`/`xhigh` levels make the server\n * reject the turn with HTTP 400 `invalid reasoning value`. Map the two\n * unsupported levels onto the closest accepted ones. Stamped in the compat\n * builder (not only at discovery) so stale-cached and custom `ollama`-provider\n * specs are backfilled on every `buildModel`, not just on a fresh\n * `omp models refresh`. Custom OpenAI-compatible providers pointed at a local\n * Ollama port under a different provider id are not covered — they must set\n * `compat.reasoningEffortMap` themselves.\n */\nconst OLLAMA_REASONING_EFFORT_MAP: ResolvedOpenAISharedCompat[\"reasoningEffortMap\"] = { minimal: \"low\", xhigh: \"max\" };\n\n/**\n * Merge the Ollama default effort map under any explicit overrides (overrides\n * win). No-op off the local `ollama` provider or for non-reasoning models.\n */\nfunction mergeOllamaReasoningEffortMap(\n\tcompat: ResolvedOpenAISharedCompat,\n\tprovider: string,\n\treasoning: boolean | undefined,\n): void {\n\tif (provider !== \"ollama\" || !reasoning) return;\n\tcompat.reasoningEffortMap = { ...OLLAMA_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && isKimiK26ModelId(spec.id)\n\t\t\t\t\t\t? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeOllamaReasoningEffortMap(compat, provider, spec.reasoning);\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeOllamaReasoningEffortMap(variant, provider, spec.reasoning);\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot's Responses endpoint rejects the `detail: \"original\"`\n\t\t// image hint with a 400; every other host preserves native-resolution\n\t\t// frames (snapcompact relies on `original`). Detect Copilot by provider id\n\t\t// or base-URL host (mirroring the Anthropic compat builder) so a model\n\t\t// pointed at the Copilot host under a different provider id still clamps.\n\t\tsupportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeOllamaReasoningEffortMap(compat, spec.provider, spec.reasoning);\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "bd216f4394c5edbf3e6336429c228032b32063fc": {"t": 1785759243.7403078, "n": 32722, "h": "c77fad0e2b09c208", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 can spend several minutes reasoning before the first visible token. */\nconst KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && isKimiK26ModelId(spec.id)\n\t\t\t\t\t\t? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" && !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "0e3e5ab72a5f5ebac1d03339ca2707bc005bd617": {"t": 1785759245.1798403, "n": 32725, "h": "ac0a2990a98a994a", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 can spend several minutes reasoning before the first visible token. */\nconst KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && isKimiK26ModelId(spec.id)\n\t\t\t\t\t\t? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot and xAI OAuth reject `detail: \"original\"` (400 / 422).\n\t\t// Every other host preserves native-resolution frames (snapcompact relies\n\t\t// on `original`). Detect Copilot by provider id or base-URL host so a\n\t\t// model pointed at the Copilot host under a different provider id still\n\t\t// clamps; xai-oauth is provider-id only (same host family as paid `xai`).\n\t\tsupportsImageDetailOriginal:\n\t\t\tspec.provider !== \"xai-oauth\" &&\n\t\t\t!modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}, "d435385ab1515c1f76f552fc4f3a974030787a41": {"t": 1785759246.8027735, "n": 32685, "h": "89c5c5c88e080b63", "c": "/**\n * OpenAI-API compat builders — chat-completions and Responses flavors.\n *\n * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model\n * (from `buildModel`): detection writes a fresh record, sparse spec overrides\n * are assigned onto it in place, and conditional policies are materialized as\n * complete alternate views. Request handlers read `model.compat` fields and\n * never detect, resolve, or allocate.\n */\nimport { isFireworksFastModelId } from \"../fireworks-model-id\";\nimport { hostMatchesUrl, modelMatchesHost } from \"../hosts\";\nimport {\n\tisAnthropicNamespacedModelId,\n\tisClaudeModelId,\n\tisDeepseekModelIdOrName,\n\tisGlm52ReasoningEffortModelId,\n\tisGrokReasoningEffortCapable,\n\tisKimiK26ModelId,\n\tisKimiModelId,\n\tisMimoModelIdOrName,\n\tisQwenModelId,\n\tmodelFamilyToken,\n} from \"../identity/family\";\nimport type {\n\tModelSpec,\n\tOpenAICompat,\n\tOpenAIStreamMarkupHealingPattern,\n\tResolvedOpenAICompat,\n\tResolvedOpenAIResponsesCompat,\n\tResolvedOpenAISharedCompat,\n\tResolvedOpenRouterCompat,\n} from \"../types\";\nimport { applyCompatOverrides } from \"./apply\";\n\n/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */\nconst GLM_CODING_PLAN_MODEL_PATTERN = /(^|\\/)glm-5(?:[.-]|$)/i;\nconst GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Direct DeepSeek reasoning models stall between thinking and answer phases. */\nconst DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Kimi K2.6 can spend several minutes reasoning before the first visible token. */\nconst KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/**\n * Native Kimi K2.7 Code requires `thinking.type: \"enabled\"` and rejects\n * disabled thinking. Match the public id, its Fast variant, and the\n * `kimi-code/kimi-for-coding` alias (which keeps the family name).\n * Caller-disabled requests on non-native dialects (Fireworks `openai`,\n * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —\n * gating on `isMoonshotKimi` is the caller's responsibility.\n */\nconst KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;\n\nfunction matchesKimiK27CodeFamily(spec: ModelSpec<\"openai-completions\">): boolean {\n\tif (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;\n\treturn spec.id === \"kimi-for-coding\" && /k2\\.?7 code/i.test(spec.name ?? \"\");\n}\n/** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */\nconst XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */\nconst ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;\n/** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */\nconst LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;\nconst MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;\nconst DSML_HEALING_PROVIDERS = new Set([\n\t\"ollama\",\n\t\"ollama-cloud\",\n\t\"nvidia\",\n\t\"deepseek\",\n\t\"fireworks\",\n\t\"nanogpt\",\n\t\"opencode-go\",\n\t\"openrouter\",\n]);\n\n// Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;\n// `ollama`-provider reasoning models carry the wire-exact `low..max` effort\n// ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.\n// Custom OpenAI-compatible providers pointed at a local Ollama port under a\n// different provider id must set `compat.reasoningEffortMap` themselves.\n\nfunction resolveReasoningDisableMode(\n\tthinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"],\n): ResolvedOpenAISharedCompat[\"reasoningDisableMode\"] {\n\tswitch (thinkingFormat) {\n\t\tcase \"openrouter\":\n\t\t\treturn \"openrouter-enabled-false\";\n\t\tcase \"zai\":\n\t\t\treturn \"zai-thinking-disabled\";\n\t\tcase \"qwen\":\n\t\t\treturn \"qwen-enable-thinking-false\";\n\t\tcase \"qwen-chat-template\":\n\t\t\treturn \"qwen-template-false\";\n\t\tdefault:\n\t\t\treturn \"lowest-effort\";\n\t}\n}\n\n/**\n * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.\n * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated\n * tool-call grammars. Every other OpenAI-compatible model defaults to\n * `\"thinking\"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence\n * on OpenRouter) are recovered from `delta.content` — **except** the official\n * OpenAI endpoint (`provider: \"openai\"` + `api.openai.com`), which returns\n * structured reasoning and never leaks, so it heals nothing (returns\n * `undefined`) to avoid misfiring on legitimate fenced content.\n */\nfunction detectStreamMarkupHealingPattern(\n\tprovider: string,\n\tmodelId: string,\n\tbaseUrl: string,\n): OpenAIStreamMarkupHealingPattern | undefined {\n\tif (provider === \"kimi-code\" || provider === \"moonshot\" || /kimi[-/_.]?k2/i.test(modelId)) {\n\t\treturn \"kimi\";\n\t}\n\tif (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {\n\t\treturn \"dsml\";\n\t}\n\tif (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;\n\treturn \"thinking\";\n}\n\n/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */\nfunction isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {\n\tif (provider !== \"openai\") return false;\n\tif (!baseUrl) return true;\n\ttry {\n\t\treturn new URL(baseUrl).hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`\n * on the request's thinking state for every model they front (Kimi K2.x,\n * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra\n * inputs are not permitted` when thinking is off but the field is supplied\n * (#1071), and 400 with `thinking is enabled but reasoning_content is missing\n * in assistant tool call message at index N` (#1484) when thinking is on and\n * the field is absent. The base compat therefore leaves the replay off, and\n * this `whenThinking` policy reactivates it for thinking-engaged requests.\n * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the\n * same path: the gateway specifically requires `reasoning_content`, and the\n * synthetic-friendly default would echo whichever field the upstream streamed\n * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong\n * key and re-triggering the 400.\n */\nconst OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat[\"whenThinking\"]> = {\n\trequiresReasoningContentForToolCalls: true,\n\tallowsSyntheticReasoningContentForToolCalls: false,\n\treasoningContentField: \"reasoning_content\",\n};\n\nconst MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat[\"reasoningEffortMap\"]> = {\n\tminimal: \"low\",\n\txhigh: \"high\",\n};\n\nfunction mergeMimoReasoningEffortMap(compat: ResolvedOpenAISharedCompat, enabled: boolean): void {\n\tif (!enabled) return;\n\tcompat.reasoningEffortMap = { ...MIMO_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };\n}\n\nfunction detectStrictModeSupport(provider: string, baseUrl: string): boolean {\n\tif (\n\t\tprovider === \"openai\" ||\n\t\tprovider === \"openrouter\" ||\n\t\tprovider === \"cerebras\" ||\n\t\tprovider === \"together\" ||\n\t\tprovider === \"github-copilot\" ||\n\t\tprovider === \"zenmux\"\n\t) {\n\t\treturn true;\n\t}\n\treturn (\n\t\thostMatchesUrl(baseUrl, \"openai\") ||\n\t\thostMatchesUrl(baseUrl, \"azureOpenAI\") ||\n\t\thostMatchesUrl(baseUrl, \"cerebras\") ||\n\t\thostMatchesUrl(baseUrl, \"together\") ||\n\t\thostMatchesUrl(baseUrl, \"openrouter\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\")\n\t);\n}\n\n/**\n * Local OpenAI-compatible inference servers whose chat templates re-tokenize\n * the entire prompt every request — llama.cpp prefix-KV-cache reuse only\n * survives when the rendered tokens stay byte-identical across turns. The\n * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these\n * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so\n * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant\n * turn's `<think>` block from `reasoning_content` (#3528).\n */\nconst LOCAL_OPENAI_COMPAT_PROVIDERS = new Set([\"llama.cpp\", \"lm-studio\", \"vllm\", \"ollama\"]);\n\n/**\n * Local proxy providers that share the loopback-default baseUrl but forward\n * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a\n * chat-template renderer themselves — `replayReasoningContent` would push\n * `reasoning_content` to the upstream, which gains no KV-cache benefit and\n * may 400 on the extra field. Excluded from BOTH the provider check above\n * and the loopback heuristic below; users who want the replay on a custom\n * proxy setup can opt in via the sparse `compat.replayReasoningContent`\n * override.\n */\nconst PROXY_OPENAI_COMPAT_PROVIDERS = new Set([\"litellm\"]);\n\nfunction hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {\n\tif (!baseUrl) return false;\n\tlet hostname: string;\n\ttry {\n\t\thostname = new URL(baseUrl).hostname.toLowerCase();\n\t} catch {\n\t\treturn false;\n\t}\n\tif (\n\t\thostname === \"localhost\" ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"0.0.0.0\" ||\n\t\thostname === \"::1\" ||\n\t\thostname === \"[::1]\"\n\t) {\n\t\treturn true;\n\t}\n\tif (/^10\\./.test(hostname)) return true;\n\tif (/^192\\.168\\./.test(hostname)) return true;\n\tif (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(hostname)) return true;\n\tif (hostname.endsWith(\".local\")) return true;\n\treturn false;\n}\n\n/**\n * Build the resolved chat-completions compat record for a model spec.\n * Provider takes precedence over URL-based detection since it's explicitly configured.\n */\nexport function buildOpenAICompat(spec: ModelSpec<\"openai-completions\">): ResolvedOpenAICompat {\n\tconst provider = spec.provider;\n\tconst baseUrl = spec.baseUrl;\n\tconst hostModel = { provider, baseUrl };\n\n\tconst isCerebras = modelMatchesHost(hostModel, \"cerebras\");\n\tconst isZai = modelMatchesHost(hostModel, \"zai\");\n\tconst isZhipu = modelMatchesHost(hostModel, \"zhipu\");\n\tconst supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);\n\tconst isKilo = modelMatchesHost(hostModel, \"kilo\");\n\tconst isKimiModel = isKimiModelId(spec.id);\n\tconst isMoonshotNative = modelMatchesHost(hostModel, \"moonshotNative\");\n\tconst isMoonshotKimi = isKimiModel && isMoonshotNative;\n\tconst requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);\n\tconst usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);\n\tconst isAnthropicModel =\n\t\tmodelMatchesHost(hostModel, \"anthropic\") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);\n\tconst isAlibaba = modelMatchesHost(hostModel, \"alibabaDashscope\");\n\tconst isNvidiaNim = modelMatchesHost(hostModel, \"nvidia\");\n\tconst isQwen = isQwenModelId(spec.id);\n\t// DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in\n\t// thinking mode unless prior assistant tool-call turns include `reasoning_content`. The\n\t// upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,\n\t// Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by\n\t// provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only\n\t// applies when thinking mode is actually engaged.\n\tconst lowerId = spec.id.toLowerCase();\n\tconst lowerName = (spec.name ?? \"\").toLowerCase();\n\tconst isXiaomiHost = modelMatchesHost(hostModel, \"xiaomi\");\n\tconst isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\tconst isMimoReasoningEffortModel =\n\t\t!isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? \"\"));\n\t// OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream\n\t// 400s come from DeepSeek and require exact reasoning_content replay.\n\tconst isOpenCodeDeepseekAlias =\n\t\tprovider === \"opencode-zen\" && (lowerId === \"big-pickle\" || lowerName === \"big pickle\");\n\tconst isDeepseekFamily =\n\t\tmodelMatchesHost(hostModel, \"deepseekFamily\") ||\n\t\tisDeepseekModelIdOrName(spec.id) ||\n\t\tisDeepseekModelIdOrName(spec.name ?? \"\") ||\n\t\tisOpenCodeDeepseekAlias;\n\tconst isDirectDeepseekApi = modelMatchesHost(hostModel, \"deepseekDirect\");\n\tconst isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekFamily && Boolean(spec.reasoning);\n\tconst isGrok = modelMatchesHost(hostModel, \"xai\");\n\tconst isMistral = modelMatchesHost(hostModel, \"mistral\");\n\tconst isOpenCodeHost = modelMatchesHost(hostModel, \"opencode\");\n\tconst isNonStandard =\n\t\tisCerebras ||\n\t\tisGrok ||\n\t\tisMistral ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"deepseekFamily\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisAlibaba ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\tisKilo ||\n\t\tisQwen ||\n\t\tisXiaomiHost ||\n\t\tisMoonshotNative ||\n\t\tisOpenCodeHost;\n\tconst isOpenCodeProvider = provider === \"opencode-go\" || provider === \"opencode-zen\";\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst useMaxTokens =\n\t\tisMistral ||\n\t\tisMoonshotNative ||\n\t\tisZai ||\n\t\tisZhipu ||\n\t\thostMatchesUrl(baseUrl, \"chutes\") ||\n\t\thostMatchesUrl(baseUrl, \"fireworks\") ||\n\t\tisDirectDeepseekApi;\n\n\t// Hosts whose chat-completions endpoints are known to accept multiple\n\t// leading `system`/`developer` messages (preferred for KV-cache reuse).\n\t// Anything outside this allowlist defaults to coalescing because\n\t// strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject\n\t// follow-up system messages with a 400.\n\tconst isOpenAIHost = modelMatchesHost(hostModel, \"openai\");\n\tconst isAzureHost = modelMatchesHost(hostModel, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost(hostModel, \"openrouter\");\n\tconst isVercelGateway = modelMatchesHost(hostModel, \"vercelAIGateway\");\n\tconst isTogether = modelMatchesHost(hostModel, \"together\");\n\tconst isFireworks = hostMatchesUrl(baseUrl, \"fireworks\");\n\tconst isGroqHost = modelMatchesHost(hostModel, \"groq\");\n\tconst isCopilotHost = provider === \"github-copilot\";\n\tconst isZenmuxHost = provider === \"zenmux\";\n\t// Endpoints/models that MUST receive a single system block. MiniMax's OpenAI\n\t// endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat\n\t// template raises \"System message must be at the beginning\" / 500s with an\n\t// internal_server_error when any system block appears past index 0. That\n\t// template ships with the weights, so every Qwen-serving vLLM/SGLang host\n\t// hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks\n\t// (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the\n\t// Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.\n\tconst isMiniMaxHost = modelMatchesHost(hostModel, \"minimax\");\n\tconst isQwenPortal = modelMatchesHost(hostModel, \"qwenPortal\");\n\tconst supportsMultipleSystemMessagesDefault =\n\t\t!isMiniMaxHost &&\n\t\t!isAlibaba &&\n\t\t!isQwenPortal &&\n\t\t!isQwen &&\n\t\t(isOpenAIHost ||\n\t\t\tisAzureHost ||\n\t\t\tisOpenRouter ||\n\t\t\tisCerebras ||\n\t\t\tisTogether ||\n\t\t\tisFireworks ||\n\t\t\tisGroqHost ||\n\t\t\tisDeepseekFamily ||\n\t\t\tisMistral ||\n\t\t\tisGrok ||\n\t\t\tisZai ||\n\t\t\tisZhipu ||\n\t\t\tisCopilotHost ||\n\t\t\tisZenmuxHost);\n\n\t// Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct\n\t// DeepSeek reasoning models, and local OpenAI-compatible backends can idle\n\t// for minutes while reasoning or cold-loading weights; widen the idle\n\t// timeout so warm-ups stop aborting and retrying.\n\tconst streamIdleTimeoutMs =\n\t\tGLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)\n\t\t\t? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: provider === \"alibaba-coding-plan\"\n\t\t\t\t? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t: isXiaomiMimo\n\t\t\t\t\t? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t: spec.reasoning && isKimiK26ModelId(spec.id)\n\t\t\t\t\t\t? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t: spec.reasoning && isDirectDeepseekApi\n\t\t\t\t\t\t\t? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t: isLocalOpenAICompatBackend\n\t\t\t\t\t\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t\t\t\t\t\t: undefined;\n\n\t// Fireworks \"Fast\" variants (`<id>-fast`) are served from the router\n\t// namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather\n\t// than the `models/` namespace the rest of the `fireworks` provider uses.\n\tconst isFireworksFastRouter = provider === \"fireworks\" && isFireworksFastModelId(spec.id);\n\tconst wireModelIdMode: ResolvedOpenAISharedCompat[\"wireModelIdMode\"] =\n\t\tprovider === \"firepass\" || isFireworksFastRouter\n\t\t\t? \"firepass\"\n\t\t\t: provider === \"fireworks\"\n\t\t\t\t? \"fireworks\"\n\t\t\t\t: isOpenRouter\n\t\t\t\t\t? \"openrouter\"\n\t\t\t\t\t: \"raw\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] =\n\t\tisZai || isZhipu || isMoonshotKimi || isXiaomiMimo\n\t\t\t? \"zai\"\n\t\t\t: isOpenRouter\n\t\t\t\t? \"openrouter\"\n\t\t\t\t: isQwen && isNvidiaNim\n\t\t\t\t\t? \"qwen-chat-template\"\n\t\t\t\t\t: isQwen && isFireworks\n\t\t\t\t\t\t? \"openai\"\n\t\t\t\t\t\t: isAlibaba || isQwen\n\t\t\t\t\t\t\t? \"qwen\"\n\t\t\t\t\t\t\t: \"openai\";\n\n\tconst compat: ResolvedOpenAICompat = {\n\t\tsupportsStore: !isNonStandard,\n\t\t// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost\n\t\t// every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors\n\t\t// the schema exactly) treats it as an unknown role: Moonshot returns a 400 \"tokenization\n\t\t// failed\", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require\n\t\t// callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror\n\t\t// OpenAI's reasoning-API surface.\n\t\tsupportsDeveloperRole: isOpenAIHost || isAzureHost,\n\t\tsupportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,\n\t\tsupportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),\n\t\t// GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.\n\t\tsupportsReasoningParams: provider !== \"github-copilot\",\n\t\treasoningEffortMap: isMimoReasoningEffortModel ? MIMO_REASONING_EFFORT_MAP : {},\n\t\tsupportsUsageInStreaming: !isCerebras,\n\t\t// pi-ai's thinking-loop guard is gemini-only; default the flag from the\n\t\t// family classifier so OpenAI-compat proxies serving Gemini are covered.\n\t\t// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === \"gemini\",\n\t\t// Kimi (including via OpenRouter and Fireworks router-form IDs such as\n\t\t// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on\n\t\t// max_tokens, not actual output. The official Kimi K2 model guidance\n\t\t// (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for\n\t\t// every call since the family can otherwise emit very long reasoning traces\n\t\t// before the final answer.\n\t\talwaysSendMaxTokens: isKimiModel,\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,\n\t\tsupportsToolChoice: !isDirectDeepseekReasoning,\n\t\tsupportsForcedToolChoice: !requiresEnabledThinking,\n\t\tsupportsNamedToolChoice: provider !== \"llama.cpp\",\n\t\tmaxTokensField: useMaxTokens ? \"max_tokens\" : \"max_completion_tokens\",\n\t\trequiresToolResultName: isMistral,\n\t\trequiresAssistantAfterToolResult: isMistral,\n\t\trequiresThinkingAsText: isMistral,\n\t\trequiresMistralToolIds: isMistral,\n\t\t// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)\n\t\t// speak the z.ai binary `thinking: { type }` field. Kimi reached through\n\t\t// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,\n\t\t// etc. — drives reasoning via OpenAI-style `reasoning_effort`\n\t\t// (low|medium|high|xhigh|max|none), so those stay on the \"openai\" path.\n\t\t// NVIDIA NIM hosts Qwen with the vLLM convention\n\t\t// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`\n\t\t// is rejected by NIM's `additionalProperties: false` request schema\n\t\t// (issue #2299).\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: true,\n\t\tfilterReasoningHistory: isOpenRouter && isAnthropicModel,\n\t\tthinkingKeep: usesMoonshotKimiPreservedThinking ? \"all\" : undefined,\n\t\treasoningContentField: \"reasoning_content\",\n\t\t// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:\n\t\t// - Kimi: documented invariant on its native API.\n\t\t// - DeepSeek-family reasoning models, including aliased OpenCode Zen models\n\t\t// like `big-pickle`, validate exact thinking-mode replay.\n\t\t// - Xiaomi MiMo models require exact `reasoning_content` replay on\n\t\t// thinking-mode tool-call continuations across standard and Token Plan hosts.\n\t\t// - Any reasoning-capable model reached through OpenRouter can enforce this\n\t\t// server-side whenever the request is in thinking mode. We can't translate\n\t\t// Anthropic's redacted/encrypted reasoning into provider-native plaintext,\n\t\t// so cross-provider continuations rely on a placeholder.\n\t\t// OpenCode Kimi aliases handle reasoning content internally and reject\n\t\t// client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path\n\t\t// (the `whenThinking` policy below re-enables the replay for thinking turns).\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel && !isOpenCodeProvider) ||\n\t\t\t(isDeepseekFamily && Boolean(spec.reasoning)) ||\n\t\t\tisXiaomiMimo ||\n\t\t\t(isOpenRouter && Boolean(spec.reasoning)),\n\t\trequiresReasoningContentForAllAssistantTurns:\n\t\t\t((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,\n\t\t// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (\".\") on tool-call turns.\n\t\t// Kimi and OpenRouter accept them when actual reasoning is unavailable.\n\t\tallowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,\n\t\t// Local llama.cpp-style servers re-tokenize the entire chat-template\n\t\t// prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct\n\t\t// the prior assistant turn's `<think>` block from `reasoning_content`,\n\t\t// so dropping the field re-renders the assistant turn without thinking\n\t\t// content and forces full prompt re-processing (#3528). The\n\t\t// `requires*ReasoningContent*` flags above stay off for these hosts —\n\t\t// they accept but don't validate the field — so the encoder needs a\n\t\t// distinct opt-in to replay on every reasoning turn. NOT gated on\n\t\t// `spec.reasoning`: the runtime discovery paths for `llama.cpp` /\n\t\t// `lm-studio` / `openai-models-list` hardcode `reasoning: false`\n\t\t// because the upstream `/models` endpoints don't advertise the\n\t\t// capability, but the OpenAI stream parser still records incoming\n\t\t// `reasoning_content` deltas as thinking blocks. Gating on the spec\n\t\t// flag would leave every discovered local Qwen / DeepSeek model\n\t\t// re-triggering #3528. The encoder only writes `reasoning_content`\n\t\t// when a thinking block actually exists on the turn\n\t\t// (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on\n\t\t// pure-text histories.\n\t\treplayReasoningContent: isLocalOpenAICompatBackend,\n\t\t// `preserve_thinking: true` makes the Qwen3.6+ chat template render\n\t\t// `<think>...</think>` for older assistant turns too, instead of\n\t\t// stripping it the moment a new user message moves them past\n\t\t// `last_query_index`. Without it, the slot's KV cache (which holds the\n\t\t// raw `<think>X</think>` tokens emitted during generation) diverges\n\t\t// from the next-turn render and llama.cpp falls back to full prompt\n\t\t// re-processing — the exact symptom reported in #3541. Auto-enabled\n\t\t// for Qwen thinking dialects on local llama.cpp-style backends (paired\n\t\t// with `replayReasoningContent` above). Non-Qwen templates ignore the\n\t\t// parameter, so the flag stays a no-op outside the Qwen path.\n\t\tqwenPreserveThinking:\n\t\t\t(thinkingFormat === \"qwen\" || thinkingFormat === \"qwen-chat-template\") && isLocalOpenAICompatBackend,\n\t\trequiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,\n\t\tcacheControlFormat: isOpenRouter && spec.id.startsWith(\"anthropic/\") ? \"anthropic\" : undefined,\n\t\topenRouterRouting: undefined,\n\t\tvercelGatewayRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode,\n\t\tisVercelGatewayHost: isVercelGateway,\n\t\tsupportsStrictMode: detectStrictModeSupport(provider, baseUrl),\n\t\textraBody: isDirectDeepseekReasoning ? { thinking: { type: \"enabled\" } } : undefined,\n\t\ttoolStrictMode: isCerebras ? \"all_strict\" : \"mixed\",\n\t\ttoolSchemaFlavor: isMoonshotNative ? \"moonshot-mfjs\" : undefined,\n\t\tstreamIdleTimeoutMs,\n\t\tstripDeepseekSpecialTokens:\n\t\t\tisDeepseekModelIdOrName(spec.id) && (provider === \"nvidia\" || provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),\n\t\temptyLengthFinishIsContextError: provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: provider === \"openai\",\n\t\tpromptCacheSessionHeader: isGrok ? \"x-grok-conv-id\" : undefined,\n\t\tdropThinkingWhenReasoningEffort: provider === \"fireworks\",\n\t};\n\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = requiresEnabledThinking\n\t\t\t? \"omit\"\n\t\t\t: resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\tmergeMimoReasoningEffortMap(compat, isMimoReasoningEffortModel);\n\n\tconst whenThinkingPolicy =\n\t\tspec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);\n\tif (whenThinkingPolicy) {\n\t\tconst variant: ResolvedOpenAICompat = { ...compat };\n\t\tapplyCompatOverrides(variant, whenThinkingPolicy);\n\t\tif (whenThinkingPolicy.reasoningDisableMode === undefined) {\n\t\t\tvariant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);\n\t\t}\n\t\tif (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {\n\t\t\tvariant.omitReasoningEffort = true;\n\t\t}\n\t\tmergeMimoReasoningEffortMap(variant, isMimoReasoningEffortModel);\n\t\tcompat.whenThinking = variant;\n\t}\n\n\treturn compat;\n}\n\ninterface OpenAIResponsesSpecLike {\n\tid?: string;\n\tprovider: string;\n\tname: string;\n\tbaseUrl: string;\n\treasoning?: boolean;\n\tcompat?: OpenAICompat;\n}\n\n/**\n * Build the resolved Responses-API compat record. Most shared OpenAI-compatible\n * capability defaults intentionally mirror chat-completions, while Responses-\n * only behavior (developer role, prompt cache, pairing strictness, image detail)\n * keeps endpoint-specific detection. Azure is detected by provider id as well\n * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-\n * resource, resolved at runtime) — while OpenAI/Copilot developer-role and\n * prompt-cache detection stay URL-keyed, as the historical call sites were.\n */\nexport function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {\n\tconst baseUrl = spec.baseUrl ?? \"\";\n\tconst isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, \"azureOpenAI\");\n\tconst isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, \"openrouter\");\n\tconst isOpenAIUrl = hostMatchesUrl(baseUrl, \"openai\");\n\tconst id = spec.id ?? \"\";\n\tconst thinkingFormat: ResolvedOpenAISharedCompat[\"thinkingFormat\"] = isOpenRouter ? \"openrouter\" : \"openai\";\n\tconst isKimiModel = id ? isKimiModelId(id) : false;\n\tconst isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;\n\tconst isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;\n\tconst reasoningCapable = Boolean(spec.reasoning);\n\tconst isLocalOpenAICompatBackend =\n\t\t!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&\n\t\t(LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));\n\n\tconst compat: ResolvedOpenAIResponsesCompat = {\n\t\tsupportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, \"githubCopilot\"),\n\t\tsupportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),\n\t\tsupportsReasoningEffort: spec.provider !== \"xai-oauth\" || isGrokReasoningEffortCapable(id),\n\t\tsupportsLongPromptCacheRetention: isOpenAIUrl,\n\t\t// Azure OpenAI and GitHub Copilot Responses paths require tool results\n\t\t// to strictly match prior tool calls when building Responses inputs.\n\t\tstrictResponsesPairing: isAzure || spec.provider === \"github-copilot\",\n\t\t// GitHub Copilot's Responses endpoint rejects the `detail: \"original\"`\n\t\t// image hint with a 400; every other host preserves native-resolution\n\t\t// frames (snapcompact relies on `original`). Detect Copilot by provider id\n\t\t// or base-URL host (mirroring the Anthropic compat builder) so a model\n\t\t// pointed at the Copilot host under a different provider id still clamps.\n\t\tsupportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, \"githubCopilot\"),\n\t\treasoningEffortMap: {},\n\t\tsupportsReasoningParams: true,\n\t\tthinkingFormat,\n\t\treasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),\n\t\tomitReasoningEffort: false,\n\t\tincludeEncryptedReasoning: spec.provider !== \"xai-oauth\",\n\t\tfilterReasoningHistory: spec.provider === \"xai-oauth\" || (isOpenRouter && isAnthropicModel),\n\t\tdisableReasoningOnForcedToolChoice: isKimiModel,\n\t\tdisableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tsupportsToolChoice: true,\n\t\tsupportsForcedToolChoice: true,\n\t\tsupportsNamedToolChoice: true,\n\t\treasoningContentField: \"reasoning_content\",\n\t\trequiresReasoningContentForToolCalls:\n\t\t\t(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&\n\t\t\treasoningCapable,\n\t\trequiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,\n\t\tallowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,\n\t\t// The Responses API replays reasoning through encrypted `summary` items,\n\t\t// not via a top-level `reasoning_content` field — this flag is\n\t\t// chat-completions-only.\n\t\treplayReasoningContent: false,\n\t\t// Responses-only; the Qwen `preserve_thinking` template knob lives on\n\t\t// the chat-completions wire shape, never on Responses.\n\t\tqwenPreserveThinking: false,\n\t\trequiresThinkingAsText: false,\n\t\trequiresMistralToolIds: false,\n\t\trequiresToolResultName: false,\n\t\trequiresAssistantAfterToolResult: false,\n\t\trequiresAssistantContentForToolCalls: isKimiModel,\n\t\topenRouterRouting: undefined,\n\t\tisOpenRouterHost: isOpenRouter,\n\t\twireModelIdMode: isOpenRouter ? \"openrouter\" : \"raw\",\n\t\talwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,\n\t\tenableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? \"\") === \"gemini\",\n\t\tsupportsObfuscationOptOut: isOpenAIUrl || spec.provider === \"openai\",\n\t\tstripDeepseekSpecialTokens:\n\t\t\tBoolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === \"nvidia\" || spec.provider === \"deepseek\"),\n\t\tstreamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,\n\t\treasoningDeltasMayBeCumulative:\n\t\t\tMINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),\n\t\temptyLengthFinishIsContextError: spec.provider === \"ollama\",\n\t\tusesOpenAIToolCallIdLimit: spec.provider === \"openai\",\n\t\tpromptCacheSessionHeader: spec.provider === \"xai-oauth\" ? \"x-grok-conv-id\" : undefined,\n\t\tstreamIdleTimeoutMs: isLocalOpenAICompatBackend\n\t\t\t? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS\n\t\t\t: spec.compat?.streamIdleTimeoutMs,\n\t};\n\tapplyCompatOverrides(compat, spec.compat);\n\tif (spec.compat?.reasoningDisableMode === undefined) {\n\t\tcompat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);\n\t}\n\tif (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {\n\t\tcompat.omitReasoningEffort = true;\n\t}\n\treturn compat;\n}\n\ntype ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;\n\nfunction pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {\n\treturn {\n\t\tsupportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,\n\t\tstrictResponsesPairing: compat.strictResponsesPairing,\n\t\tsupportsImageDetailOriginal: compat.supportsImageDetailOriginal,\n\t\tsupportsObfuscationOptOut: compat.supportsObfuscationOptOut,\n\t} satisfies ResponsesOnlyCompat;\n}\n\nexport function buildOpenRouterCompat(spec: ModelSpec<\"openrouter\">): ResolvedOpenRouterCompat {\n\tconst chat = buildOpenAICompat({\n\t\t...spec,\n\t\tapi: \"openai-completions\",\n\t} as ModelSpec<\"openai-completions\">);\n\tconst responses = buildOpenAIResponsesCompat(spec);\n\treturn { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;\n}\n"}}, "packages/ai/test/deepseek-reasoning-content.test.ts": {"3a8591a8af5b6d200088d12ca75a5517cb064fa8": {"t": 1786068368.2478988, "n": 21782, "h": "217439bfc0d19c64", "c": "import { describe, expect, it } from \"bun:test\";\nimport { renderDemotedThinking } from \"@oh-my-pi/pi-ai/dialect\";\nimport { convertMessages } from \"@oh-my-pi/pi-ai/providers/openai-completions\";\nimport type { AssistantMessage, Model, ModelSpec, ThinkingContent, ToolCall } from \"@oh-my-pi/pi-ai/types\";\nimport { buildModel } from \"@oh-my-pi/pi-catalog/build\";\nimport { Effort } from \"@oh-my-pi/pi-catalog/effort\";\nimport { getBundledModel } from \"@oh-my-pi/pi-catalog/models\";\n\ninterface OpenAICompletionAssistantWireMessage {\n\trole: \"assistant\";\n\tcontent?: unknown;\n\treasoning_content?: unknown;\n\trs_6f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c?: unknown;\n}\n\nfunction isOpenAICompletionAssistantWireMessage(message: unknown): message is OpenAICompletionAssistantWireMessage {\n\tif (typeof message !== \"object\" || message === null) return false;\n\treturn (message as { role?: unknown }).role === \"assistant\";\n}\n\nfunction findOpenAICompletionAssistantWireMessage(\n\tmessages: readonly unknown[] | undefined,\n): OpenAICompletionAssistantWireMessage | undefined {\n\treturn messages?.find(isOpenAICompletionAssistantWireMessage);\n}\n\nfunction deepseekModel(overrides: Partial<ModelSpec<\"openai-completions\">>): Model<\"openai-completions\"> {\n\tconst base = getBundledModel(\"openai\", \"gpt-4o-mini\");\n\treturn buildModel({\n\t\t...base,\n\t\tapi: \"openai-completions\",\n\t\treasoning: true,\n\t\tcompat: base.compatConfig,\n\t\t...overrides,\n\t} as ModelSpec<\"openai-completions\">);\n}\n\nfunction assistantToolCall(\n\tmodel: Model<\"openai-completions\">,\n\tcontent?: AssistantMessage[\"content\"],\n): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: content ?? [\n\t\t\t{\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\tid: \"call_test_1\",\n\t\t\t\tname: \"read\",\n\t\t\t\targuments: { path: \"/tmp/test\" },\n\t\t\t},\n\t\t],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: \"toolUse\",\n\t\ttimestamp: Date.now(),\n\t};\n}\n\ndescribe(\"DeepSeek reasoning_content tool-call replay\", () => {\n\t// ----------------------------------------------------------------\n\t// Fix 1: honest wire-exact ladders for DeepSeek-family on any provider —\n\t// V4 Flash exposes [low, high, max] (#7668), V4 Pro stays [high, max].\n\t// ----------------------------------------------------------------\n\tdescribe(\"thinking ladder (Fix 1)\", () => {\n\t\tit(\"bakes the honest [low, high, max] flash ladder with no effortMap on opencode-go\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\texpect(model.thinking?.efforts).toEqual([Effort.Low, Effort.High, Effort.Max]);\n\t\t\texpect(model.thinking?.effortMap).toBeUndefined();\n\t\t});\n\n\t\tit(\"bakes the honest [low, high, max] flash ladder with no effortMap on NVIDIA\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"nvidia\",\n\t\t\t\tbaseUrl: \"https://integrate.api.nvidia.com/v1\",\n\t\t\t\tid: \"deepseek-ai/deepseek-v4-flash\",\n\t\t\t});\n\t\t\texpect(model.thinking?.efforts).toEqual([Effort.Low, Effort.High, Effort.Max]);\n\t\t\texpect(model.thinking?.effortMap).toBeUndefined();\n\t\t});\n\n\t\tit(\"bakes the honest [high, max] ladder with no effortMap on the official endpoint\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"deepseek\",\n\t\t\t\tbaseUrl: \"https://api.deepseek.com/v1\",\n\t\t\t\tid: \"deepseek-v4-pro\",\n\t\t\t});\n\t\t\texpect(model.thinking?.efforts).toEqual([Effort.High, Effort.Max]);\n\t\t\texpect(model.thinking?.effortMap).toBeUndefined();\n\t\t});\n\n\t\tit(\"does NOT map xhigh for non-DeepSeek models\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"openai\",\n\t\t\t\tbaseUrl: \"https://api.openai.com/v1\",\n\t\t\t\tid: \"gpt-4o-mini\",\n\t\t\t\treasoning: false,\n\t\t\t});\n\t\t\texpect(model.thinking?.effortMap?.xhigh).toBeUndefined();\n\t\t});\n\t});\n\n\t// ----------------------------------------------------------------\n\t// allowsSyntheticReasoningContentForToolCalls flag\n\t// ----------------------------------------------------------------\n\tdescribe(\"allowsSyntheticReasoningContentForToolCalls flag\", () => {\n\t\tit(\"is false for DeepSeek-family reasoning models\", () => {\n\t\t\tconst compat = deepseekModel({\n\t\t\t\tprovider: \"deepseek\",\n\t\t\t\tbaseUrl: \"https://api.deepseek.com/v1\",\n\t\t\t\tid: \"deepseek-v4-pro\",\n\t\t\t}).compat;\n\t\t\texpect(compat.allowsSyntheticReasoningContentForToolCalls).toBe(false);\n\t\t});\n\n\t\tit(\"is false for DeepSeek-family on NVIDIA\", () => {\n\t\t\tconst compat = deepseekModel({\n\t\t\t\tprovider: \"nvidia\",\n\t\t\t\tbaseUrl: \"https://integrate.api.nvidia.com/v1\",\n\t\t\t\tid: \"deepseek-ai/deepseek-v4-flash\",\n\t\t\t}).compat;\n\t\t\texpect(compat.allowsSyntheticReasoningContentForToolCalls).toBe(false);\n\t\t});\n\n\t\tit(\"is true for non-DeepSeek reasoning models on OpenRouter\", () => {\n\t\t\tconst base = getBundledModel(\"openai\", \"gpt-4o-mini\");\n\t\t\tconst compat = buildModel({\n\t\t\t\t...base,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"openrouter\",\n\t\t\t\tbaseUrl: \"https://openrouter.ai/api/v1\",\n\t\t\t\tid: \"qwen/qwq-32b\",\n\t\t\t\treasoning: true,\n\t\t\t\tcompat: base.compatConfig,\n\t\t\t} as ModelSpec<\"openai-completions\">).compat;\n\t\t\t// Qwen is not isDeepseekFamily, so synthetic is allowed\n\t\t\texpect(compat.allowsSyntheticReasoningContentForToolCalls).toBe(true);\n\t\t});\n\t});\n\n\t// ----------------------------------------------------------------\n\t// Fix 2: reasoning_content from empty thinking blocks with signature\n\t// ----------------------------------------------------------------\n\tdescribe(\"thinking-block signature recovery (Fix 2)\", () => {\n\t\tit(\"recovers reasoning_content from empty thinking block with valid signature\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\t// Simulate a tool-call turn with an empty thinking block that has a valid\n\t\t\t// signature — this happens when reasoning text was lost but the signature\n\t\t\t// (field name) is preserved.\n\t\t\tconst msg: AssistantMessage = {\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\tthinking: \"\",\n\t\t\t\t\t\tthinkingSignature: \"reasoning_content\",\n\t\t\t\t\t} as ThinkingContent,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\tid: \"call_empty_thinking\",\n\t\t\t\t\t\tname: \"read\",\n\t\t\t\t\t\targuments: { path: \"/tmp/test\" },\n\t\t\t\t\t} as ToolCall,\n\t\t\t\t],\n\t\t\t\tapi: model.api,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tusage: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t},\n\t\t\t\tstopReason: \"toolUse\",\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\t// The reasoning_content field should be set from the signature, even if empty.\n\t\t\texpect(assistant?.reasoning_content).toBe(\"\");\n\t\t});\n\n\t\tit(\"recovers reasoning_content from non-empty thinking block with signature\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\tconst msg: AssistantMessage = {\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\tthinking: \"I need to read the file first.\",\n\t\t\t\t\t\tthinkingSignature: \"reasoning_content\",\n\t\t\t\t\t} as ThinkingContent,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\tid: \"call_with_thinking\",\n\t\t\t\t\t\tname: \"read\",\n\t\t\t\t\t\targuments: { path: \"/tmp/test\" },\n\t\t\t\t\t} as ToolCall,\n\t\t\t\t],\n\t\t\t\tapi: model.api,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tusage: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t},\n\t\t\t\tstopReason: \"toolUse\",\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.reasoning_content).toBe(\"I need to read the file first.\");\n\t\t});\n\n\t\tit(\"normalizes OpenRouter reasoning deltas to DeepSeek reasoning_content on replay\", () => {\n\t\t\tconst model = getBundledModel(\"openrouter\", \"deepseek/deepseek-v4-pro\") as Model<\"openai-completions\">;\n\t\t\tconst compat = model.compat;\n\t\t\texpect(compat.requiresReasoningContentForToolCalls).toBe(true);\n\t\t\texpect(compat.allowsSyntheticReasoningContentForToolCalls).toBe(false);\n\n\t\t\tconst msg = assistantToolCall(model, [\n\t\t\t\t{\n\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\tthinking: \"I should inspect the requested file.\",\n\t\t\t\t\tthinkingSignature: \"reasoning\",\n\t\t\t\t} as ThinkingContent,\n\t\t\t\t{\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: \"call_openrouter_deepseek\",\n\t\t\t\t\tname: \"read\",\n\t\t\t\t\targuments: { path: \"package.json\" },\n\t\t\t\t} as ToolCall,\n\t\t\t]);\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.reasoning_content).toBe(\"I should inspect the requested file.\");\n\t\t});\n\t\tit(\"does not use opaque signature as property name but still sets reasoning_content from thinking text\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\t// Simulate a thinking block with an opaque signature from another provider\n\t\t\t// (e.g. Anthropic encrypted signature, OpenAI Responses JSON item).\n\t\t\t// The code should NOT write to a property named after the opaque signature.\n\t\t\t// It should still set reasoning_content from the thinking text via the\n\t\t\t// existing thinkingFormat=\"openai\" path.\n\t\t\tconst msg: AssistantMessage = {\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\tthinking: \"some reasoning\",\n\t\t\t\t\t\tthinkingSignature: \"rs_6f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c\",\n\t\t\t\t\t} as ThinkingContent,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\tid: \"call_opaque_sig\",\n\t\t\t\t\t\tname: \"read\",\n\t\t\t\t\t\targuments: { path: \"/tmp/test\" },\n\t\t\t\t\t} as ToolCall,\n\t\t\t\t],\n\t\t\t\tapi: model.api,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tusage: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t},\n\t\t\t\tstopReason: \"toolUse\",\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\t// Should NOT have used the opaque signature as a property name.\n\t\t\texpect(assistant?.rs_6f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c).toBeUndefined();\n\t\t\t// Should have set reasoning_content from the thinking text via the openai path.\n\t\t\texpect(assistant?.reasoning_content).toBe(\"some reasoning\");\n\t\t});\n\t\tit(\"demotes cross-api foreign thinking while satisfying tool-call reasoning_content schema\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\tconst msg = assistantToolCall(model, [\n\t\t\t\t{\n\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\tthinking: \"Need to preserve cross-api reasoning.\",\n\t\t\t\t\tthinkingSignature: \"sig_from_anthropic\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: \"toolu_cross_api\",\n\t\t\t\t\tname: \"read\",\n\t\t\t\t\targuments: { path: \"README.md\" },\n\t\t\t\t},\n\t\t\t]);\n\t\t\tmsg.api = \"anthropic-messages\";\n\t\t\tmsg.provider = \"zai\";\n\t\t\tmsg.model = \"claude-compatible\";\n\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.reasoning_content).toBe(\"\");\n\t\t\texpect(assistant?.content).toBe(renderDemotedThinking(model.id, \"Need to preserve cross-api reasoning.\"));\n\t\t});\n\t\tit(\"falls through to empty-string when thinking block has opaque signature and empty text\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\t// Empty-text thinking block with opaque signature — Tier 1 should reject the\n\t\t\t// opaque signature, nonEmptyThinkingBlocks won't include it, and the openai path\n\t\t\t// won't set anything. Tier 2 should then emit empty reasoning_content.\n\t\t\tconst msg: AssistantMessage = {\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\tthinking: \"\",\n\t\t\t\t\t\tthinkingSignature: \"rs_6f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c\",\n\t\t\t\t\t} as ThinkingContent,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\tid: \"call_empty_opaque\",\n\t\t\t\t\t\tname: \"read\",\n\t\t\t\t\t\targuments: { path: \"/tmp/test\" },\n\t\t\t\t\t} as ToolCall,\n\t\t\t\t],\n\t\t\t\tapi: model.api,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tusage: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t},\n\t\t\t\tstopReason: \"toolUse\",\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.rs_6f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c).toBeUndefined();\n\t\t\texpect(assistant?.reasoning_content).toBe(\"\");\n\t\t});\n\t});\n\n\t// ----------------------------------------------------------------\n\t// Fix 3: Empty-string fallback when NO thinking blocks exist\n\t// (matches the actual observed 400 failure: proxy-stripped reasoning)\n\t// ----------------------------------------------------------------\n\tdescribe(\"empty-string fallback for missing reasoning_content (Fix 3)\", () => {\n\t\tit(\"sets reasoning_content to empty string when no thinking blocks exist for DeepSeek\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\t// Tool-call turn with NO thinking blocks at all — matches the actual\n\t\t\t// observed 400 error pattern where proxy stripped reasoning_content.\n\t\t\tconst msg = assistantToolCall(model, [\n\t\t\t\t{\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: \"call_no_thinking\",\n\t\t\t\t\tname: \"read\",\n\t\t\t\t\targuments: { path: \"/tmp/test\" },\n\t\t\t\t} as ToolCall,\n\t\t\t]);\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\t// reasoning_content must be present (empty string) — not absent and not \".\"\n\t\t\tconst rc = assistant?.reasoning_content;\n\t\t\texpect(rc).toBeDefined();\n\t\t\texpect(rc).toBe(\"\");\n\t\t});\n\n\t\tit(\"sets reasoning_content to empty string for OpenCode Zen big-pickle tool-call turns\", () => {\n\t\t\tconst model = getBundledModel(\"opencode-zen\", \"big-pickle\") as Model<\"openai-completions\">;\n\t\t\tconst compat = model.compat;\n\t\t\texpect(compat.requiresReasoningContentForToolCalls).toBe(true);\n\t\t\texpect(compat.allowsSyntheticReasoningContentForToolCalls).toBe(false);\n\n\t\t\tconst msg = assistantToolCall(model, [\n\t\t\t\t{\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: \"call_big_pickle\",\n\t\t\t\t\tname: \"bash\",\n\t\t\t\t\targuments: { command: \"git status --short\" },\n\t\t\t\t} as ToolCall,\n\t\t\t]);\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.reasoning_content).toBe(\"\");\n\t\t\texpect(assistant?.content).toBe(\"\");\n\t\t});\n\n\t\tit(\"sets content to empty string (not null) when reasoning_content is present\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"nvidia\",\n\t\t\t\tbaseUrl: \"https://integrate.api.nvidia.com/v1\",\n\t\t\t\tid: \"deepseek-ai/deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\tconst msg = assistantToolCall(model, [\n\t\t\t\t{\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: \"call_no_content\",\n\t\t\t\t\tname: \"list_files\",\n\t\t\t\t\targuments: { path: \".\" },\n\t\t\t\t} as ToolCall,\n\t\t\t]);\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.content).toBe(\"\");\n\t\t});\n\t});\n\n\t// ----------------------------------------------------------------\n\t// Fix 4: reasoning_content on ALL assistant turns, not just tool-call turns\n\t// DeepSeek V4 requires reasoning_content on every assistant message once any\n\t// prior turn included it — including plain text responses with no tool calls.\n\t// ----------------------------------------------------------------\n\tdescribe(\"reasoning_content on non-tool-call assistant turns (Fix 4)\", () => {\n\t\tit(\"injects empty reasoning_content on plain text assistant turn for DeepSeek\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"deepseek\",\n\t\t\t\tbaseUrl: \"https://api.deepseek.com/v1\",\n\t\t\t\tid: \"deepseek-v4-pro\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\t// Plain text assistant response — no tool calls, no thinking blocks.\n\t\t\t// This is the exact pattern from the observed 400 error.\n\t\t\tconst msg: AssistantMessage = {\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: [{ type: \"text\", text: \"Here is the answer to your question.\" }],\n\t\t\t\tapi: model.api,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tusage: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t},\n\t\t\t\tstopReason: \"stop\",\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\t// reasoning_content must be present — even on non-tool-call turns\n\t\t\tconst rc = assistant?.reasoning_content;\n\t\t\texpect(rc).toBeDefined();\n\t\t\texpect(rc).toBe(\"\");\n\t\t});\n\n\t\tit(\"injects reasoning_content from thinking blocks on plain text assistant turn\", () => {\n\t\t\tconst model = deepseekModel({\n\t\t\t\tprovider: \"opencode-go\",\n\t\t\t\tbaseUrl: \"https://opencode.ai/zen/go/v1\",\n\t\t\t\tid: \"deepseek-v4-flash\",\n\t\t\t});\n\t\t\tconst compat = model.compat;\n\t\t\tconst msg: AssistantMessage = {\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\tthinking: \"Let me think about this.\",\n\t\t\t\t\t\tthinkingSignature: \"reasoning_content\",\n\t\t\t\t\t} as ThinkingContent,\n\t\t\t\t\t{ type: \"text\", text: \"The answer is 42.\" },\n\t\t\t\t],\n\t\t\t\tapi: model.api,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tusage: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t},\n\t\t\t\tstopReason: \"stop\",\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.reasoning_content).toBe(\"Let me think about this.\");\n\t\t\texpect(assistant?.content).toBe(\"The answer is 42.\");\n\t\t});\n\n\t\tit(\"does NOT inject reasoning_content on non-tool-call turn for non-DeepSeek providers\", () => {\n\t\t\tconst base = getBundledModel(\"openai\", \"gpt-4o-mini\");\n\t\t\tconst model: Model<\"openai-completions\"> = buildModel({\n\t\t\t\t...base,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"openrouter\",\n\t\t\t\tbaseUrl: \"https://openrouter.ai/api/v1\",\n\t\t\t\tid: \"qwen/qwq-32b\",\n\t\t\t\treasoning: true,\n\t\t\t\tcompat: base.compatConfig,\n\t\t\t} as ModelSpec<\"openai-completions\">);\n\t\t\tconst compat = model.compat;\n\t\t\tconst msg: AssistantMessage = {\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: [{ type: \"text\", text: \"Plain answer.\" }],\n\t\t\t\tapi: model.api,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tusage: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t},\n\t\t\t\tstopReason: \"stop\",\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\t// OpenRouter reasoning models only need reasoning_content on tool-call turns\n\t\t\texpect(assistant?.reasoning_content).toBeUndefined();\n\t\t});\n\t});\n\n\t// ----------------------------------------------------------------\n\t// Tier 3: Synthetic placeholder for non-DeepSeek providers\n\t// ----------------------------------------------------------------\n\tdescribe(\"synthetic placeholder for non-DeepSeek providers (Tier 3)\", () => {\n\t\tit('still uses \".\" placeholder for Kimi models that accept it', () => {\n\t\t\tconst base = getBundledModel(\"openai\", \"gpt-4o-mini\");\n\t\t\tconst model: Model<\"openai-completions\"> = buildModel({\n\t\t\t\t...base,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tprovider: \"moonshot\",\n\t\t\t\tbaseUrl: \"https://api.moonshot.ai/v1\",\n\t\t\t\tid: \"kimi-k2.5\",\n\t\t\t\treasoning: true,\n\t\t\t\tcompat: base.compatConfig,\n\t\t\t} as ModelSpec<\"openai-completions\">);\n\t\t\tconst compat = model.compat;\n\t\t\texpect(compat.requiresReasoningContentForToolCalls).toBe(true);\n\t\t\texpect(compat.allowsSyntheticReasoningContentForToolCalls).toBe(true);\n\t\t\tconst msg = assistantToolCall(model, [\n\t\t\t\t{\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: \"call_kimi\",\n\t\t\t\t\tname: \"read\",\n\t\t\t\t\targuments: { path: \"/tmp\" },\n\t\t\t\t} as ToolCall,\n\t\t\t]);\n\t\t\tconst messages = convertMessages(model, { messages: [msg] }, compat);\n\t\t\tconst assistant = findOpenAICompletionAssistantWireMessage(messages);\n\t\t\texpect(assistant).toBeDefined();\n\t\t\texpect(assistant?.reasoning_content).toBe(\".\");\n\t\t});\n\t});\n});\n"}}}}