jeopi-catalog 16.2.13

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