@linxiraos/pi-catalog 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +999 -0
  2. package/README.md +43 -0
  3. package/dist/types/build.d.ts +14 -0
  4. package/dist/types/compat/anthropic.d.ts +18 -0
  5. package/dist/types/compat/apply.d.ts +7 -0
  6. package/dist/types/compat/bedrock.d.ts +3 -0
  7. package/dist/types/compat/devin.d.ts +9 -0
  8. package/dist/types/compat/openai.d.ts +26 -0
  9. package/dist/types/discovery/antigravity.d.ts +70 -0
  10. package/dist/types/discovery/codex.d.ts +36 -0
  11. package/dist/types/discovery/cursor-gen/agent_pb.d.ts +16769 -0
  12. package/dist/types/discovery/cursor.d.ts +23 -0
  13. package/dist/types/discovery/devin-gen/buf/validate/validate_pb.d.ts +1715 -0
  14. package/dist/types/discovery/devin-gen/exa/analytics_pb/analytics_pb.d.ts +693 -0
  15. package/dist/types/discovery/devin-gen/exa/api_server_pb/api_server_pb.d.ts +9158 -0
  16. package/dist/types/discovery/devin-gen/exa/auth_pb/auth_pb.d.ts +52 -0
  17. package/dist/types/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.d.ts +276 -0
  18. package/dist/types/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.d.ts +78 -0
  19. package/dist/types/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.d.ts +999 -0
  20. package/dist/types/discovery/devin-gen/exa/chat_pb/chat_pb.d.ts +1704 -0
  21. package/dist/types/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.d.ts +656 -0
  22. package/dist/types/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.d.ts +15014 -0
  23. package/dist/types/discovery/devin-gen/exa/context_module_pb/context_module_pb.d.ts +607 -0
  24. package/dist/types/discovery/devin-gen/exa/cortex_pb/cortex_pb.d.ts +11830 -0
  25. package/dist/types/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.d.ts +248 -0
  26. package/dist/types/discovery/devin-gen/exa/index_pb/index_pb.d.ts +1747 -0
  27. package/dist/types/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.d.ts +509 -0
  28. package/dist/types/discovery/devin-gen/exa/language_server_pb/language_server_pb.d.ts +9048 -0
  29. package/dist/types/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.d.ts +1760 -0
  30. package/dist/types/discovery/devin-gen/exa/prompt_pb/prompt_pb.d.ts +286 -0
  31. package/dist/types/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.d.ts +405 -0
  32. package/dist/types/discovery/devin-gen/exa/trust_pb/trust_pb.d.ts +582 -0
  33. package/dist/types/discovery/devin.d.ts +24 -0
  34. package/dist/types/discovery/gemini.d.ts +25 -0
  35. package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
  36. package/dist/types/discovery/index.d.ts +5 -0
  37. package/dist/types/discovery/openai-compatible.d.ts +74 -0
  38. package/dist/types/effort.d.ts +10 -0
  39. package/dist/types/fireworks-model-id.d.ts +23 -0
  40. package/dist/types/hosts.d.ts +164 -0
  41. package/dist/types/identity/bundled.d.ts +3 -0
  42. package/dist/types/identity/classify.d.ts +69 -0
  43. package/dist/types/identity/dialect.d.ts +3 -0
  44. package/dist/types/identity/family.d.ts +181 -0
  45. package/dist/types/identity/id.d.ts +12 -0
  46. package/dist/types/identity/index.d.ts +8 -0
  47. package/dist/types/identity/markers.d.ts +4 -0
  48. package/dist/types/identity/priority.d.ts +1 -0
  49. package/dist/types/identity/reference.d.ts +29 -0
  50. package/dist/types/index.d.ts +17 -0
  51. package/dist/types/model-cache.d.ts +23 -0
  52. package/dist/types/model-manager.d.ts +77 -0
  53. package/dist/types/model-thinking.d.ts +66 -0
  54. package/dist/types/models.d.ts +12 -0
  55. package/dist/types/provider-models/bundled-references.d.ts +13 -0
  56. package/dist/types/provider-models/cache-provider-id.d.ts +9 -0
  57. package/dist/types/provider-models/descriptor-types.d.ts +76 -0
  58. package/dist/types/provider-models/descriptors.d.ts +504 -0
  59. package/dist/types/provider-models/google.d.ts +27 -0
  60. package/dist/types/provider-models/index.d.ts +7 -0
  61. package/dist/types/provider-models/ollama.d.ts +22 -0
  62. package/dist/types/provider-models/openai-compat.d.ts +599 -0
  63. package/dist/types/provider-models/special.d.ts +53 -0
  64. package/dist/types/types.d.ts +811 -0
  65. package/dist/types/utils.d.ts +20 -0
  66. package/dist/types/variant-collapse.d.ts +133 -0
  67. package/dist/types/wire/alibaba-token-plan.d.ts +22 -0
  68. package/dist/types/wire/codex.d.ts +42 -0
  69. package/dist/types/wire/coreweave.d.ts +12 -0
  70. package/dist/types/wire/gemini-headers.d.ts +37 -0
  71. package/dist/types/wire/github-copilot.d.ts +39 -0
  72. package/package.json +101 -0
  73. package/src/build.ts +94 -0
  74. package/src/compat/anthropic.ts +179 -0
  75. package/src/compat/apply.ts +15 -0
  76. package/src/compat/bedrock.ts +153 -0
  77. package/src/compat/devin.ts +12 -0
  78. package/src/compat/openai.ts +796 -0
  79. package/src/discovery/antigravity.ts +253 -0
  80. package/src/discovery/codex.ts +346 -0
  81. package/src/discovery/cursor-gen/agent_pb.ts +19513 -0
  82. package/src/discovery/cursor.ts +396 -0
  83. package/src/discovery/devin-gen/buf/validate/validate_pb.ts +1862 -0
  84. package/src/discovery/devin-gen/exa/analytics_pb/analytics_pb.ts +871 -0
  85. package/src/discovery/devin-gen/exa/api_server_pb/api_server_pb.ts +11083 -0
  86. package/src/discovery/devin-gen/exa/auth_pb/auth_pb.ts +71 -0
  87. package/src/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.ts +348 -0
  88. package/src/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.ts +103 -0
  89. package/src/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.ts +1198 -0
  90. package/src/discovery/devin-gen/exa/chat_pb/chat_pb.ts +2063 -0
  91. package/src/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.ts +810 -0
  92. package/src/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.ts +18354 -0
  93. package/src/discovery/devin-gen/exa/context_module_pb/context_module_pb.ts +732 -0
  94. package/src/discovery/devin-gen/exa/cortex_pb/cortex_pb.ts +14277 -0
  95. package/src/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.ts +312 -0
  96. package/src/discovery/devin-gen/exa/index_pb/index_pb.ts +2106 -0
  97. package/src/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.ts +623 -0
  98. package/src/discovery/devin-gen/exa/language_server_pb/language_server_pb.ts +10918 -0
  99. package/src/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.ts +2125 -0
  100. package/src/discovery/devin-gen/exa/prompt_pb/prompt_pb.ts +361 -0
  101. package/src/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.ts +450 -0
  102. package/src/discovery/devin-gen/exa/trust_pb/trust_pb.ts +712 -0
  103. package/src/discovery/devin.ts +151 -0
  104. package/src/discovery/gemini.ts +260 -0
  105. package/src/discovery/gitlab-duo-workflow.ts +859 -0
  106. package/src/discovery/index.ts +5 -0
  107. package/src/discovery/openai-compatible.ts +249 -0
  108. package/src/effort.ts +18 -0
  109. package/src/fireworks-model-id.ts +50 -0
  110. package/src/hosts.ts +155 -0
  111. package/src/identity/bundled.ts +28 -0
  112. package/src/identity/classify.ts +219 -0
  113. package/src/identity/dialect.ts +42 -0
  114. package/src/identity/family.ts +366 -0
  115. package/src/identity/id.ts +81 -0
  116. package/src/identity/index.ts +8 -0
  117. package/src/identity/markers.ts +49 -0
  118. package/src/identity/priority.ts +59 -0
  119. package/src/identity/reference.ts +167 -0
  120. package/src/index.ts +17 -0
  121. package/src/model-cache.ts +285 -0
  122. package/src/model-manager.ts +672 -0
  123. package/src/model-thinking.ts +821 -0
  124. package/src/models.json +106099 -0
  125. package/src/models.json.d.ts +9 -0
  126. package/src/models.ts +93 -0
  127. package/src/provider-models/bundled-references.ts +75 -0
  128. package/src/provider-models/cache-provider-id.ts +68 -0
  129. package/src/provider-models/descriptor-types.ts +85 -0
  130. package/src/provider-models/descriptors.ts +584 -0
  131. package/src/provider-models/google.ts +107 -0
  132. package/src/provider-models/index.ts +7 -0
  133. package/src/provider-models/ollama.ts +204 -0
  134. package/src/provider-models/openai-compat.ts +5991 -0
  135. package/src/provider-models/special.ts +213 -0
  136. package/src/types.ts +952 -0
  137. package/src/utils.ts +69 -0
  138. package/src/variant-collapse.ts +1225 -0
  139. package/src/wire/alibaba-token-plan.ts +54 -0
  140. package/src/wire/codex.ts +60 -0
  141. package/src/wire/coreweave.ts +51 -0
  142. package/src/wire/gemini-headers.ts +74 -0
  143. package/src/wire/github-copilot.ts +115 -0
@@ -0,0 +1,796 @@
1
+ /**
2
+ * OpenAI-API compat builders — chat-completions and Responses flavors.
3
+ *
4
+ * `buildOpenAICompat`/`buildOpenAIResponsesCompat` run exactly once per model
5
+ * (from `buildModel`): detection writes a fresh record, sparse spec overrides
6
+ * are assigned onto it in place, and conditional policies are materialized as
7
+ * complete alternate views. Request handlers read `model.compat` fields and
8
+ * never detect, resolve, or allocate.
9
+ */
10
+ import { isFireworksFastModelId } from "../fireworks-model-id";
11
+ import { hostMatchesUrl, modelMatchesHost } from "../hosts";
12
+ import { bareModelId, parseOpenAIModel, semverGte } from "../identity/classify";
13
+ import {
14
+ isAnthropicNamespacedModelId,
15
+ isClaudeModelId,
16
+ isDeepseekModelIdOrName,
17
+ isGlm52ReasoningEffortModelId,
18
+ isGrokReasoningEffortCapable,
19
+ isKimiK3ModelId,
20
+ isKimiK26ModelId,
21
+ isKimiModelId,
22
+ isMimoModelIdOrName,
23
+ isOpenAISamplingRestrictedModelId,
24
+ isQwenModelId,
25
+ modelFamilyToken,
26
+ } from "../identity/family";
27
+ import type {
28
+ ModelSpec,
29
+ OpenAICompat,
30
+ OpenAIStreamMarkupHealingPattern,
31
+ ResolvedOpenAICompat,
32
+ ResolvedOpenAIResponsesCompat,
33
+ ResolvedOpenAISharedCompat,
34
+ ResolvedOpenRouterCompat,
35
+ } from "../types";
36
+ import { applyCompatOverrides } from "./apply";
37
+
38
+ /** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */
39
+ const GLM_CODING_PLAN_MODEL_PATTERN = /(^|\/)glm-5(?:[.-]|$)/i;
40
+ const GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;
41
+ /** Direct DeepSeek reasoning models stall between thinking and answer phases. */
42
+ const DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;
43
+ /** Kimi K2.6 and native K2.7 Code can spend several minutes reasoning before the first visible token. */
44
+ const KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;
45
+ /**
46
+ * Native Kimi K2.7 Code requires `thinking.type: "enabled"` and rejects
47
+ * disabled thinking. Match the public id, its Fast variant, and the
48
+ * `kimi-code/kimi-for-coding` alias (which keeps the family name).
49
+ * Caller-disabled requests on non-native dialects (Fireworks `openai`,
50
+ * OpenRouter `openrouter`, …) MUST keep their per-dialect disable shape —
51
+ * gating on `isMoonshotKimi` is the caller's responsibility.
52
+ */
53
+ const KIMI_K27_CODE_MODEL_PATTERN = /(?:^|\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i;
54
+
55
+ function matchesKimiK27CodeFamily(spec: ModelSpec<"openai-completions">): boolean {
56
+ if (KIMI_K27_CODE_MODEL_PATTERN.test(spec.id)) return true;
57
+ return spec.id === "kimi-for-coding" && /k2\.?7 code/i.test(spec.name ?? "");
58
+ }
59
+ /** Xiaomi MiMo Pro on api.xiaomimimo.com can stall ~2min before the first event (issue #1770). */
60
+ const XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;
61
+ /** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */
62
+ const ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;
63
+ /** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */
64
+ const LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;
65
+ const MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;
66
+ const DSML_HEALING_PROVIDERS = new Set([
67
+ "ollama",
68
+ "ollama-cloud",
69
+ "nvidia",
70
+ "deepseek",
71
+ "fireworks",
72
+ "nanogpt",
73
+ "opencode-go",
74
+ "openrouter",
75
+ ]);
76
+
77
+ // Ollama's OpenAI-compatible `reasoning.effort` accepts `high|medium|low|max|none`;
78
+ // `ollama`-provider reasoning models carry the wire-exact `low..max` effort
79
+ // ladder (see getModelDefinedEfforts), so no compat-level remapping is needed.
80
+ // Custom OpenAI-compatible providers pointed at a local Ollama port under a
81
+ // different provider id must set `compat.reasoningEffortMap` themselves.
82
+
83
+ function resolveReasoningDisableMode(
84
+ thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"],
85
+ ): ResolvedOpenAISharedCompat["reasoningDisableMode"] {
86
+ switch (thinkingFormat) {
87
+ case "openrouter":
88
+ return "openrouter-enabled-false";
89
+ case "zai":
90
+ case "kimi":
91
+ return "zai-thinking-disabled";
92
+ case "qwen":
93
+ return "qwen-enable-thinking-false";
94
+ case "qwen-chat-template":
95
+ return "qwen-template-false";
96
+ default:
97
+ return "lowest-effort";
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.
103
+ * Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated
104
+ * tool-call grammars. Every other OpenAI-compatible model defaults to
105
+ * `"thinking"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence
106
+ * on OpenRouter) are recovered from `delta.content` — **except** the official
107
+ * OpenAI endpoint (`provider: "openai"` + `api.openai.com`), which returns
108
+ * structured reasoning and never leaks, so it heals nothing (returns
109
+ * `undefined`) to avoid misfiring on legitimate fenced content.
110
+ */
111
+ function detectStreamMarkupHealingPattern(
112
+ provider: string,
113
+ modelId: string,
114
+ baseUrl: string,
115
+ ): OpenAIStreamMarkupHealingPattern | undefined {
116
+ if (provider === "kimi-code" || provider === "moonshot" || /kimi[-/_.]?k2/i.test(modelId)) {
117
+ return "kimi";
118
+ }
119
+ if (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {
120
+ return "dsml";
121
+ }
122
+ if (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;
123
+ return "thinking";
124
+ }
125
+
126
+ /** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */
127
+ function isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {
128
+ if (provider !== "openai") return false;
129
+ if (!baseUrl) return true;
130
+ try {
131
+ return new URL(baseUrl).hostname === "api.openai.com";
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Explicit prompt-cache breakpoints are a GPT-5.6+ first-party contract. Keep
139
+ * this intentionally narrow: compatible gateways and older OpenAI models
140
+ * reject the new request fields unless their catalog compat opts in.
141
+ */
142
+ function supportsOfficialOpenAIPromptCacheBreakpoints(provider: string, modelId: string, baseUrl: string): boolean {
143
+ if (!isOfficialOpenAIEndpoint(provider, baseUrl)) return false;
144
+ const model = parseOpenAIModel(bareModelId(modelId));
145
+ return model !== null && semverGte(model.version, "5.6");
146
+ }
147
+
148
+ /**
149
+ * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`
150
+ * on the request's thinking state for every model they front (Kimi K2.x,
151
+ * DeepSeek V4, GLM-5.x, Qwen3.x, MiMo, MiniMax, …): they 400 with `Extra
152
+ * inputs are not permitted` when thinking is off but the field is supplied
153
+ * (#1071), and 400 with `thinking is enabled but reasoning_content is missing
154
+ * in assistant tool call message at index N` (#1484) when thinking is on and
155
+ * the field is absent. The base compat therefore leaves the replay off, and
156
+ * this `whenThinking` policy reactivates it for thinking-engaged requests.
157
+ * `allowsSyntheticReasoningContentForToolCalls` is forced to `false` on the
158
+ * same path: the gateway specifically requires `reasoning_content`, and the
159
+ * synthetic-friendly default would echo whichever field the upstream streamed
160
+ * (e.g. `reasoning` for many opencode turns), landing the replay in the wrong
161
+ * key and re-triggering the 400.
162
+ */
163
+ const OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat["whenThinking"]> = {
164
+ requiresReasoningContentForToolCalls: true,
165
+ allowsSyntheticReasoningContentForToolCalls: false,
166
+ reasoningContentField: "reasoning_content",
167
+ };
168
+
169
+ const KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat["reasoningEffortMap"]> = {
170
+ minimal: "low",
171
+ medium: "high",
172
+ xhigh: "max",
173
+ max: "max",
174
+ };
175
+
176
+ const MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat["reasoningEffortMap"]> = {
177
+ minimal: "low",
178
+ xhigh: "high",
179
+ };
180
+
181
+ function mergeModelReasoningEffortMap(
182
+ compat: ResolvedOpenAISharedCompat,
183
+ modelId: string,
184
+ isMimoReasoningEffortModel: boolean,
185
+ ): void {
186
+ let detected: NonNullable<OpenAICompat["reasoningEffortMap"]>;
187
+ if (isKimiK3ModelId(modelId)) {
188
+ detected = KIMI_K3_REASONING_EFFORT_MAP;
189
+ } else if (isMimoReasoningEffortModel) {
190
+ detected = MIMO_REASONING_EFFORT_MAP;
191
+ } else {
192
+ return;
193
+ }
194
+ compat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };
195
+ }
196
+
197
+ function detectStrictModeSupport(provider: string, baseUrl: string): boolean {
198
+ if (
199
+ provider === "openai" ||
200
+ provider === "openrouter" ||
201
+ provider === "cerebras" ||
202
+ provider === "together" ||
203
+ provider === "github-copilot" ||
204
+ provider === "zenmux"
205
+ ) {
206
+ return true;
207
+ }
208
+ return (
209
+ hostMatchesUrl(baseUrl, "openai") ||
210
+ hostMatchesUrl(baseUrl, "azureOpenAI") ||
211
+ hostMatchesUrl(baseUrl, "cerebras") ||
212
+ hostMatchesUrl(baseUrl, "together") ||
213
+ hostMatchesUrl(baseUrl, "openrouter") ||
214
+ hostMatchesUrl(baseUrl, "deepseekFamily")
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Local OpenAI-compatible inference servers whose chat templates re-tokenize
220
+ * the entire prompt every request — llama.cpp prefix-KV-cache reuse only
221
+ * survives when the rendered tokens stay byte-identical across turns. The
222
+ * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these
223
+ * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so
224
+ * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant
225
+ * turn's `<think>` block from `reasoning_content` (#3528).
226
+ */
227
+ const LOCAL_OPENAI_COMPAT_PROVIDERS = new Set(["llama.cpp", "lm-studio", "vllm", "ollama"]);
228
+
229
+ /** Hosts that accept only none/auto/required rather than a named tool-choice object. */
230
+ const STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS: Record<string, true> = {
231
+ "llama.cpp": true,
232
+ "lm-studio": true,
233
+ };
234
+
235
+ /**
236
+ * Local proxy providers that share the loopback-default baseUrl but forward
237
+ * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a
238
+ * chat-template renderer themselves — `replayReasoningContent` would push
239
+ * `reasoning_content` to the upstream, which gains no KV-cache benefit and
240
+ * may 400 on the extra field. Excluded from BOTH the provider check above
241
+ * and the loopback heuristic below; users who want the replay on a custom
242
+ * proxy setup can opt in via the sparse `compat.replayReasoningContent`
243
+ * override.
244
+ */
245
+ const PROXY_OPENAI_COMPAT_PROVIDERS = new Set(["litellm"]);
246
+
247
+ function hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {
248
+ if (!baseUrl) return false;
249
+ let hostname: string;
250
+ try {
251
+ hostname = new URL(baseUrl).hostname.toLowerCase();
252
+ } catch {
253
+ return false;
254
+ }
255
+ if (
256
+ hostname === "localhost" ||
257
+ hostname === "127.0.0.1" ||
258
+ hostname === "0.0.0.0" ||
259
+ hostname === "::1" ||
260
+ hostname === "[::1]"
261
+ ) {
262
+ return true;
263
+ }
264
+ if (/^10\./.test(hostname)) return true;
265
+ if (/^192\.168\./.test(hostname)) return true;
266
+ if (/^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname)) return true;
267
+ if (hostname.endsWith(".local")) return true;
268
+ return false;
269
+ }
270
+
271
+ /**
272
+ * Build the resolved chat-completions compat record for a model spec.
273
+ * Provider takes precedence over URL-based detection since it's explicitly configured.
274
+ */
275
+ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): ResolvedOpenAICompat {
276
+ const provider = spec.provider;
277
+ const baseUrl = spec.baseUrl;
278
+ const hostModel = { provider, baseUrl };
279
+
280
+ const isCerebras = modelMatchesHost(hostModel, "cerebras");
281
+ const isZai = modelMatchesHost(hostModel, "zai");
282
+ const isZhipu = modelMatchesHost(hostModel, "zhipu");
283
+ const supportsZaiReasoningEffort = (isZai || isZhipu) && isGlm52ReasoningEffortModelId(spec.id);
284
+ const isKilo = modelMatchesHost(hostModel, "kilo");
285
+ const isKimiModel = isKimiModelId(spec.id);
286
+ const isMoonshotNative = modelMatchesHost(hostModel, "moonshotNative");
287
+ const isMoonshotKimi = isKimiModel && isMoonshotNative;
288
+ // Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory
289
+ // low/high/max thinking, not the K2.x binary `thinking: { type }` block.
290
+ const isKimiK3 = isKimiK3ModelId(spec.id);
291
+ const isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;
292
+ const requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);
293
+ const usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);
294
+ const isAnthropicModel =
295
+ modelMatchesHost(hostModel, "anthropic") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id);
296
+ const isAlibaba = modelMatchesHost(hostModel, "alibabaDashscope");
297
+ const isNvidiaNim = modelMatchesHost(hostModel, "nvidia");
298
+ const isQwen = isQwenModelId(spec.id);
299
+ // DeepSeek V4 (and other reasoning-capable DeepSeek models) reject follow-up requests in
300
+ // thinking mode unless prior assistant tool-call turns include `reasoning_content`. The
301
+ // upstream model is reachable through many OpenAI-compat hosts (api.deepseek.com, Deepinfra,
302
+ // Kilo, NVIDIA NIM, Zenmux, OpenRouter, …), so we match by model id/name as well as by
303
+ // provider/baseUrl. The flag is gated by `spec.reasoning` because the invariant only
304
+ // applies when thinking mode is actually engaged.
305
+ const lowerId = spec.id.toLowerCase();
306
+ const lowerName = (spec.name ?? "").toLowerCase();
307
+ const isXiaomiHost = modelMatchesHost(hostModel, "xiaomi");
308
+ const isXiaomiMimo = isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? ""));
309
+ const isMimoReasoningEffortModel =
310
+ !isXiaomiHost && (isMimoModelIdOrName(spec.id) || isMimoModelIdOrName(spec.name ?? ""));
311
+ // OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream
312
+ // 400s come from DeepSeek and require exact reasoning_content replay.
313
+ const isOpenCodeDeepseekAlias =
314
+ provider === "opencode-zen" && (lowerId === "big-pickle" || lowerName === "big pickle");
315
+ const isDeepseekFamily =
316
+ modelMatchesHost(hostModel, "deepseekFamily") ||
317
+ isDeepseekModelIdOrName(spec.id) ||
318
+ isDeepseekModelIdOrName(spec.name ?? "") ||
319
+ isOpenCodeDeepseekAlias;
320
+ const isDirectDeepseekApi = modelMatchesHost(hostModel, "deepseekDirect");
321
+ const isDeepseekReasoning = isDeepseekFamily && Boolean(spec.reasoning);
322
+ const isDirectDeepseekReasoning = isDirectDeepseekApi && isDeepseekReasoning;
323
+ const isGrok = modelMatchesHost(hostModel, "xai");
324
+ const isMistral = modelMatchesHost(hostModel, "mistral");
325
+ const isOpenCodeHost = modelMatchesHost(hostModel, "opencode");
326
+ // Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)
327
+ // implements a subset of chat-completions and 400s on `store` ("Unknown name \"store\"").
328
+ const isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, "googleAistudio");
329
+ const isNonStandard =
330
+ isCerebras ||
331
+ isGrok ||
332
+ isMistral ||
333
+ isGoogleAistudioOpenAI ||
334
+ hostMatchesUrl(baseUrl, "chutes") ||
335
+ hostMatchesUrl(baseUrl, "deepseekFamily") ||
336
+ hostMatchesUrl(baseUrl, "fireworks") ||
337
+ isAlibaba ||
338
+ isZai ||
339
+ isZhipu ||
340
+ isKilo ||
341
+ isQwen ||
342
+ isXiaomiHost ||
343
+ isMoonshotNative ||
344
+ isOpenCodeHost;
345
+ const isOpenCodeProvider = provider === "opencode-go" || provider === "opencode-zen";
346
+ const isLocalOpenAICompatBackend =
347
+ !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
348
+ (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));
349
+ // Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING
350
+ // local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:
351
+ // widening the first-event/idle abort ceiling only helps a slow local
352
+ // upstream and never pushes an extra wire field, so the proxy carve-out (a
353
+ // `replayReasoningContent` safety measure) must not also strip the timeout
354
+ // floor. Without this, a loopback litellm fronting a cold/reprocessing
355
+ // llama-server aborts prefill at the 100s default and retry-loops (#4786).
356
+ const isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);
357
+
358
+ const useMaxTokens =
359
+ isMistral ||
360
+ isMoonshotNative ||
361
+ isZai ||
362
+ isZhipu ||
363
+ hostMatchesUrl(baseUrl, "chutes") ||
364
+ hostMatchesUrl(baseUrl, "fireworks") ||
365
+ isDirectDeepseekApi;
366
+
367
+ const supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(provider, spec.id, baseUrl);
368
+ // Hosts whose chat-completions endpoints are known to accept multiple
369
+ // leading `system`/`developer` messages (preferred for KV-cache reuse).
370
+ // Anything outside this allowlist defaults to coalescing because
371
+ // strict chat templates (Qwen 3.5+ via vLLM, MiniMax, etc.) reject
372
+ // follow-up system messages with a 400.
373
+ const isOpenAIHost = modelMatchesHost(hostModel, "openai");
374
+ const isAzureHost = modelMatchesHost(hostModel, "azureOpenAI");
375
+ const isOpenRouter = modelMatchesHost(hostModel, "openrouter");
376
+ const isVercelGateway = modelMatchesHost(hostModel, "vercelAIGateway");
377
+ const isTogether = modelMatchesHost(hostModel, "together");
378
+ const isFireworks = hostMatchesUrl(baseUrl, "fireworks");
379
+ const isGroqHost = modelMatchesHost(hostModel, "groq");
380
+ const isCopilotHost = provider === "github-copilot";
381
+ const isZenmuxHost = provider === "zenmux";
382
+ // Endpoints/models that MUST receive a single system block. MiniMax's OpenAI
383
+ // endpoint returns error 2013 on multiple system messages; the Qwen 3.5+ chat
384
+ // template raises "System message must be at the beginning" / 500s with an
385
+ // internal_server_error when any system block appears past index 0. That
386
+ // template ships with the weights, so every Qwen-serving vLLM/SGLang host
387
+ // hits it — confirmed on Alibaba Dashscope, Qwen Portal, and Fireworks
388
+ // (`fireworks/qwen3.7-plus` 500'd on two leading system blocks). Gate on the
389
+ // Qwen family itself, not per-host: coalescing only trades away KV-cache reuse.
390
+ const isMiniMaxHost = modelMatchesHost(hostModel, "minimax");
391
+ const isQwenPortal = modelMatchesHost(hostModel, "qwenPortal");
392
+ const supportsMultipleSystemMessagesDefault =
393
+ !isMiniMaxHost &&
394
+ !isAlibaba &&
395
+ !isQwenPortal &&
396
+ !isQwen &&
397
+ (isOpenAIHost ||
398
+ isAzureHost ||
399
+ isOpenRouter ||
400
+ isCerebras ||
401
+ isTogether ||
402
+ isFireworks ||
403
+ isGroqHost ||
404
+ isDeepseekFamily ||
405
+ isMistral ||
406
+ isGrok ||
407
+ isZai ||
408
+ isZhipu ||
409
+ isCopilotHost ||
410
+ isZenmuxHost);
411
+
412
+ // Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct
413
+ // DeepSeek reasoning models, and local OpenAI-compatible backends can idle
414
+ // for minutes while reasoning or cold-loading weights; widen the idle
415
+ // timeout so warm-ups stop aborting and retrying.
416
+ const streamIdleTimeoutMs =
417
+ GLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu || isOpenCodeHost)
418
+ ? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS
419
+ : provider === "alibaba-coding-plan"
420
+ ? ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS
421
+ : isXiaomiMimo
422
+ ? XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS
423
+ : spec.reasoning &&
424
+ (isKimiK26ModelId(spec.id) ||
425
+ isMoonshotKimiK3 ||
426
+ (isMoonshotKimi && matchesKimiK27CodeFamily(spec)))
427
+ ? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS
428
+ : spec.reasoning && isDirectDeepseekApi
429
+ ? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS
430
+ : isLocalServingBackend
431
+ ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
432
+ : undefined;
433
+
434
+ // Fireworks "Fast" variants (`<id>-fast`) are served from the router
435
+ // namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather
436
+ // than the `models/` namespace the rest of the `fireworks` provider uses.
437
+ const isFireworksFastRouter = provider === "fireworks" && isFireworksFastModelId(spec.id);
438
+ const wireModelIdMode: ResolvedOpenAISharedCompat["wireModelIdMode"] =
439
+ provider === "firepass" || isFireworksFastRouter
440
+ ? "firepass"
441
+ : provider === "fireworks"
442
+ ? "fireworks"
443
+ : isOpenRouter
444
+ ? "openrouter"
445
+ : "raw";
446
+ const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] =
447
+ (isMoonshotKimi && !isMoonshotKimiK3) || isZai || isZhipu || isXiaomiMimo
448
+ ? "zai"
449
+ : isOpenRouter
450
+ ? "openrouter"
451
+ : isQwen && isNvidiaNim
452
+ ? "qwen-chat-template"
453
+ : isQwen && isFireworks
454
+ ? "openai"
455
+ : isAlibaba || isQwen
456
+ ? "qwen"
457
+ : "openai";
458
+
459
+ const compat: ResolvedOpenAICompat = {
460
+ supportsStore: !isNonStandard,
461
+ // `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost
462
+ // every OpenAI-compatible host other than OpenAI itself (and Azure OpenAI, which mirrors
463
+ // the schema exactly) treats it as an unknown role: Moonshot returns a 400 "tokenization
464
+ // failed", Groq/Cerebras/etc. error or silently misroute. Default to `system` and require
465
+ // callers to opt in via `compat.supportsDeveloperRole: true` for hosts known to mirror
466
+ // OpenAI's reasoning-API surface.
467
+ supportsDeveloperRole: isOpenAIHost || isAzureHost,
468
+ supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
469
+ supportsReasoningEffort: !isGrok && !isXiaomiMimo && (!(isZai || isZhipu) || supportsZaiReasoningEffort),
470
+ // GitHub Copilot's chat-completions endpoint rejects reasoning params wholesale.
471
+ supportsReasoningParams: provider !== "github-copilot",
472
+ // OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
473
+ // temperature/top_p/… with a 400 on every serving host (#5606).
474
+ supportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),
475
+ reasoningEffortMap: {},
476
+ supportsUsageInStreaming: !isCerebras,
477
+ // pi-ai's thinking-loop guard is gemini-only; default the flag from the
478
+ // family classifier so OpenAI-compat proxies serving Gemini are covered.
479
+ // An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.
480
+ enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === "gemini",
481
+ // Kimi (including via OpenRouter and Fireworks router-form IDs such as
482
+ // `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on
483
+ // max_tokens, not actual output. The official Kimi K2 model guidance
484
+ // (https://docs.fireworks.ai/models/kimi-k2) also requires `max_tokens` for
485
+ // every call since the family can otherwise emit very long reasoning traces
486
+ // before the final answer.
487
+ alwaysSendMaxTokens: isKimiModel,
488
+ // Native Kimi K3 always reasons through `reasoning_effort` (never the
489
+ // K2.x binary `thinking` block that #827's forced-tool-choice conflict is
490
+ // about), so suppressing its effort would leave K3 in an unsupported mode.
491
+ disableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,
492
+ disableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,
493
+ supportsToolChoice: !isDirectDeepseekReasoning,
494
+ // DeepSeek reasoning models on OpenCode Zen/Go 400 with
495
+ // "Thinking mode does not support this tool_choice" when a specific
496
+ // function is forced while the gateway's default thinking mode is active.
497
+ // Downgrade only on those gateways: other hosts can turn thinking off via
498
+ // disableReasoningOnToolChoice and must retain hard tool selection.
499
+ supportsForcedToolChoice: !requiresEnabledThinking && !(isOpenCodeHost && isDeepseekReasoning),
500
+ supportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[provider] !== true,
501
+ maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
502
+ requiresToolResultName: isMistral,
503
+ requiresAssistantAfterToolResult: isMistral,
504
+ requiresThinkingAsText: isMistral,
505
+ requiresMistralToolIds: isMistral,
506
+ // Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by
507
+ // `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.
508
+ // K3 and Kimi reached through OpenAI-compatible proxies drive reasoning
509
+ // via OpenAI-style `reasoning_effort`.
510
+ // NVIDIA NIM hosts Qwen with the vLLM convention
511
+ // (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`
512
+ // is rejected by NIM's `additionalProperties: false` request schema
513
+ // (issue #2299).
514
+ thinkingFormat,
515
+ kimiApiFormat: undefined,
516
+ reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
517
+ omitReasoningEffort: false,
518
+ includeEncryptedReasoning: true,
519
+ filterReasoningHistory: isOpenRouter && isAnthropicModel,
520
+ thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined,
521
+ reasoningContentField: "reasoning_content",
522
+ // Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:
523
+ // - Kimi: documented invariant on its native API.
524
+ // - DeepSeek-family reasoning models, including aliased OpenCode Zen models
525
+ // like `big-pickle`, validate exact thinking-mode replay.
526
+ // - Xiaomi MiMo models require exact `reasoning_content` replay on
527
+ // thinking-mode tool-call continuations across standard and Token Plan hosts.
528
+ // - Any reasoning-capable model reached through OpenRouter can enforce this
529
+ // server-side whenever the request is in thinking mode. We can't translate
530
+ // Anthropic's redacted/encrypted reasoning into provider-native plaintext,
531
+ // so cross-provider continuations rely on a placeholder.
532
+ // OpenCode Kimi aliases handle reasoning content internally and reject
533
+ // client-sent `reasoning_content`, so exclude only that Kimi-on-OpenCode path
534
+ // (the `whenThinking` policy below re-enables the replay for thinking turns).
535
+ requiresReasoningContentForToolCalls:
536
+ (isKimiModel && !isOpenCodeProvider) ||
537
+ (isDeepseekFamily && Boolean(spec.reasoning)) ||
538
+ isXiaomiMimo ||
539
+ (isOpenRouter && Boolean(spec.reasoning)),
540
+ requiresReasoningContentForAllAssistantTurns:
541
+ ((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,
542
+ // DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (".") on tool-call turns.
543
+ // Kimi and OpenRouter accept them when actual reasoning is unavailable.
544
+ allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,
545
+ // Local llama.cpp-style servers re-tokenize the entire chat-template
546
+ // prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct
547
+ // the prior assistant turn's `<think>` block from `reasoning_content`,
548
+ // so dropping the field re-renders the assistant turn without thinking
549
+ // content and forces full prompt re-processing (#3528). The
550
+ // `requires*ReasoningContent*` flags above stay off for these hosts —
551
+ // they accept but don't validate the field — so the encoder needs a
552
+ // distinct opt-in to replay on every reasoning turn. NOT gated on
553
+ // `spec.reasoning`: the runtime discovery paths for `llama.cpp` /
554
+ // `lm-studio` / `openai-models-list` hardcode `reasoning: false`
555
+ // because the upstream `/models` endpoints don't advertise the
556
+ // capability, but the OpenAI stream parser still records incoming
557
+ // `reasoning_content` deltas as thinking blocks. Gating on the spec
558
+ // flag would leave every discovered local Qwen / DeepSeek model
559
+ // re-triggering #3528. The encoder only writes `reasoning_content`
560
+ // when a thinking block actually exists on the turn
561
+ // (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on
562
+ // pure-text histories.
563
+ replayReasoningContent: isLocalOpenAICompatBackend,
564
+ // `preserve_thinking: true` makes the Qwen3.6+ chat template render
565
+ // `<think>...</think>` for older assistant turns too, instead of
566
+ // stripping it the moment a new user message moves them past
567
+ // `last_query_index`. Without it, the slot's KV cache (which holds the
568
+ // raw `<think>X</think>` tokens emitted during generation) diverges
569
+ // from the next-turn render and llama.cpp falls back to full prompt
570
+ // re-processing — the exact symptom reported in #3541. Auto-enabled
571
+ // for Qwen thinking dialects on local llama.cpp-style backends (paired
572
+ // with `replayReasoningContent` above). Non-Qwen templates ignore the
573
+ // parameter, so the flag stays a no-op outside the Qwen path.
574
+ qwenPreserveThinking:
575
+ (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && isLocalOpenAICompatBackend,
576
+ requiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,
577
+ cacheControlFormat: isOpenRouter && spec.id.startsWith("anthropic/") ? "anthropic" : undefined,
578
+ supportsPromptCacheBreakpoints,
579
+ promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined,
580
+ openRouterRouting: undefined,
581
+ vercelGatewayRouting: undefined,
582
+ isOpenRouterHost: isOpenRouter,
583
+ wireModelIdMode,
584
+ isVercelGatewayHost: isVercelGateway,
585
+ supportsStrictMode: detectStrictModeSupport(provider, baseUrl),
586
+ extraBody: undefined,
587
+ toolStrictMode: isCerebras ? "all_strict" : "mixed",
588
+ // Kimi-family ids trigger MFJS on any host, not just native base URLs:
589
+ // proxies (OpenRouter, custom gateways) forward `tools.function.parameters`
590
+ // to Moonshot verbatim, which 400s on non-MFJS constructs.
591
+ toolSchemaFlavor:
592
+ isMoonshotNative || isKimiModel ? "moonshot-mfjs" : isLocalOpenAICompatBackend ? "grammar" : undefined,
593
+ streamFirstEventTimeoutMs: isLocalServingBackend ? 0 : undefined,
594
+ streamIdleTimeoutMs,
595
+ stripDeepseekSpecialTokens:
596
+ isDeepseekModelIdOrName(spec.id) && (provider === "nvidia" || provider === "deepseek"),
597
+ streamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),
598
+ reasoningDeltasMayBeCumulative:
599
+ MINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),
600
+ emptyLengthFinishIsContextError: provider === "ollama",
601
+ usesOpenAIToolCallIdLimit: provider === "openai",
602
+ promptCacheSessionHeader: isGrok ? "x-grok-conv-id" : undefined,
603
+ dropThinkingWhenReasoningEffort: provider === "fireworks",
604
+ };
605
+
606
+ applyCompatOverrides(compat, spec.compat);
607
+ const deepseekThinking = compat.extraBody?.thinking;
608
+ if (
609
+ isDirectDeepseekReasoning &&
610
+ typeof deepseekThinking === "object" &&
611
+ deepseekThinking !== null &&
612
+ "type" in deepseekThinking &&
613
+ deepseekThinking.type === "enabled"
614
+ ) {
615
+ const extraBody = { ...compat.extraBody };
616
+ delete extraBody.thinking;
617
+ compat.extraBody = Object.keys(extraBody).length > 0 ? extraBody : undefined;
618
+ }
619
+ if (spec.compat?.reasoningDisableMode === undefined) {
620
+ compat.reasoningDisableMode = requiresEnabledThinking
621
+ ? "omit"
622
+ : isDirectDeepseekReasoning
623
+ ? "zai-thinking-disabled"
624
+ : resolveReasoningDisableMode(compat.thinkingFormat);
625
+ }
626
+ if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
627
+ compat.omitReasoningEffort = true;
628
+ }
629
+ mergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);
630
+
631
+ const whenThinkingPolicy =
632
+ spec.compat?.whenThinking ??
633
+ (isDirectDeepseekReasoning
634
+ ? { extraBody: { ...compat.extraBody, thinking: { type: "enabled" } } }
635
+ : isOpenCodeProvider && spec.reasoning
636
+ ? OPENCODE_WHEN_THINKING
637
+ : undefined);
638
+ if (whenThinkingPolicy) {
639
+ const variant: ResolvedOpenAICompat = { ...compat };
640
+ applyCompatOverrides(variant, whenThinkingPolicy);
641
+ if (whenThinkingPolicy.reasoningDisableMode === undefined) {
642
+ variant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);
643
+ }
644
+ if (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {
645
+ variant.omitReasoningEffort = true;
646
+ }
647
+ mergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);
648
+ compat.whenThinking = variant;
649
+ }
650
+
651
+ return compat;
652
+ }
653
+
654
+ interface OpenAIResponsesSpecLike {
655
+ id?: string;
656
+ provider: string;
657
+ name: string;
658
+ baseUrl: string;
659
+ reasoning?: boolean;
660
+ compat?: OpenAICompat;
661
+ }
662
+
663
+ /**
664
+ * Build the resolved Responses-API compat record. Most shared OpenAI-compatible
665
+ * capability defaults intentionally mirror chat-completions, while Responses-
666
+ * only behavior (developer role, prompt cache, pairing strictness, image detail)
667
+ * keeps endpoint-specific detection. Azure is detected by provider id as well
668
+ * as URL — bundled `azure` models carry no baseUrl (the deployment host is per-
669
+ * resource, resolved at runtime) — while OpenAI/Copilot developer-role and
670
+ * prompt-cache detection stay URL-keyed, as the historical call sites were.
671
+ */
672
+ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {
673
+ const baseUrl = spec.baseUrl ?? "";
674
+ const isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, "azureOpenAI");
675
+ const isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, "openrouter");
676
+ const isOpenAIUrl = hostMatchesUrl(baseUrl, "openai");
677
+ const isVercelGateway = modelMatchesHost({ provider: spec.provider, baseUrl }, "vercelAIGateway");
678
+ const id = spec.id ?? "";
679
+ const supportsPromptCacheBreakpoints = supportsOfficialOpenAIPromptCacheBreakpoints(spec.provider, id, baseUrl);
680
+ const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] = isOpenRouter ? "openrouter" : "openai";
681
+ const isKimiModel = id ? isKimiModelId(id) : false;
682
+ const isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;
683
+ const isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;
684
+ const reasoningCapable = Boolean(spec.reasoning);
685
+ // `replayReasoningContent` is Responses-only-false, so the proxy carve-out is
686
+ // irrelevant here; the stream-timeout floor still applies to ANY loopback /
687
+ // RFC1918 backend, including local proxies (litellm), so a slow local
688
+ // upstream is not aborted at the 100s default and retry-looped (#4786).
689
+ const isLocalServingBackend =
690
+ (!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||
691
+ hasLocalLoopbackBaseUrl(baseUrl);
692
+
693
+ const compat: ResolvedOpenAIResponsesCompat = {
694
+ supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
695
+ supportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),
696
+ supportsReasoningEffort: spec.provider !== "xai-oauth" || isGrokReasoningEffortCapable(id),
697
+ supportsLongPromptCacheRetention: isOpenAIUrl,
698
+ supportsPromptCacheBreakpoints,
699
+ promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined,
700
+ // Azure OpenAI and GitHub Copilot Responses paths require tool results
701
+ // to strictly match prior tool calls when building Responses inputs.
702
+ strictResponsesPairing: isAzure || spec.provider === "github-copilot",
703
+ // GitHub Copilot and xAI OAuth reject `detail: "original"` (400 / 422).
704
+ // Every other host preserves native-resolution frames (snapcompact relies
705
+ // on `original`). Detect Copilot by provider id or base-URL host so a
706
+ // model pointed at the Copilot host under a different provider id still
707
+ // clamps; xai-oauth is provider-id only (same host family as paid `xai`).
708
+ supportsImageDetailOriginal:
709
+ spec.provider !== "xai-oauth" && !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
710
+ reasoningEffortMap: {},
711
+ supportsReasoningParams: true,
712
+ // OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
713
+ // temperature/top_p/… with a 400 on every serving host (#5606).
714
+ supportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),
715
+ thinkingFormat,
716
+ reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
717
+ omitReasoningEffort: false,
718
+ includeEncryptedReasoning: spec.provider !== "xai-oauth",
719
+ filterReasoningHistory: spec.provider === "xai-oauth" || (isOpenRouter && isAnthropicModel),
720
+ disableReasoningOnForcedToolChoice: isKimiModel,
721
+ disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,
722
+ supportsToolChoice: true,
723
+ supportsForcedToolChoice: true,
724
+ supportsNamedToolChoice: STRING_ONLY_NAMED_TOOL_CHOICE_PROVIDERS[spec.provider] !== true,
725
+ reasoningContentField: "reasoning_content",
726
+ requiresReasoningContentForToolCalls:
727
+ (isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&
728
+ reasoningCapable,
729
+ requiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,
730
+ allowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,
731
+ // The Responses API replays reasoning through encrypted `summary` items,
732
+ // not via a top-level `reasoning_content` field — this flag is
733
+ // chat-completions-only.
734
+ replayReasoningContent: false,
735
+ // Responses-only; the Qwen `preserve_thinking` template knob lives on
736
+ // the chat-completions wire shape, never on Responses.
737
+ qwenPreserveThinking: false,
738
+ requiresThinkingAsText: false,
739
+ requiresMistralToolIds: false,
740
+ requiresToolResultName: false,
741
+ requiresAssistantAfterToolResult: false,
742
+ requiresAssistantContentForToolCalls: isKimiModel,
743
+ openRouterRouting: undefined,
744
+ vercelGatewayRouting: undefined,
745
+ isOpenRouterHost: isOpenRouter,
746
+ isVercelGatewayHost: isVercelGateway,
747
+ wireModelIdMode: isOpenRouter ? "openrouter" : "raw",
748
+ // Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still
749
+ // lands on Moonshot's MFJS validator.
750
+ toolSchemaFlavor: isKimiModel ? "moonshot-mfjs" : undefined,
751
+ alwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,
752
+ enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? "") === "gemini",
753
+ supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
754
+ stripDeepseekSpecialTokens:
755
+ Boolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === "nvidia" || spec.provider === "deepseek"),
756
+ streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,
757
+ reasoningDeltasMayBeCumulative:
758
+ MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),
759
+ emptyLengthFinishIsContextError: spec.provider === "ollama",
760
+ usesOpenAIToolCallIdLimit: spec.provider === "openai",
761
+ promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
762
+ streamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,
763
+ streamIdleTimeoutMs: isLocalServingBackend
764
+ ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
765
+ : spec.compat?.streamIdleTimeoutMs,
766
+ };
767
+ applyCompatOverrides(compat, spec.compat);
768
+ if (spec.compat?.reasoningDisableMode === undefined) {
769
+ compat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);
770
+ }
771
+ if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
772
+ compat.omitReasoningEffort = true;
773
+ }
774
+ return compat;
775
+ }
776
+
777
+ type ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;
778
+
779
+ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {
780
+ return {
781
+ supportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,
782
+ strictResponsesPairing: compat.strictResponsesPairing,
783
+ supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
784
+ supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
785
+ isVercelGatewayHost: compat.isVercelGatewayHost,
786
+ } satisfies ResponsesOnlyCompat;
787
+ }
788
+
789
+ export function buildOpenRouterCompat(spec: ModelSpec<"openrouter">): ResolvedOpenRouterCompat {
790
+ const chat = buildOpenAICompat({
791
+ ...spec,
792
+ api: "openai-completions",
793
+ } as ModelSpec<"openai-completions">);
794
+ const responses = buildOpenAIResponsesCompat(spec);
795
+ return { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;
796
+ }