@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -110,6 +110,11 @@ export interface ProviderRegistryEntry {
110
110
  allowKeyAuthOverride?: boolean;
111
111
  allowPrivateNetworkByDefault?: boolean;
112
112
  keyOptional?: boolean;
113
+ /**
114
+ * Registry-only key-login policy for public model catalogs that cannot authenticate a key.
115
+ * The dashboard flow then reports the key as unverifiable instead of a false positive.
116
+ */
117
+ apiKeyValidation?: "unknown";
113
118
  /**
114
119
  * Free-tier pricing (no paid subscription required). Distinct from `keyOptional`:
115
120
  * free tiers may still require an API key (e.g. NVIDIA NIM free credits).
@@ -148,6 +153,25 @@ export interface ProviderRegistryEntry {
148
153
  * of paying a translation hop.
149
154
  */
150
155
  modelWireDefaults?: Record<string, ModelWireDefault>;
156
+ /**
157
+ * Registry-only per-model override for the upstream request shape used behind a
158
+ * Codex Responses WebSocket turn. `false` keeps the client-facing WebSocket but
159
+ * asks the upstream Responses endpoint for bounded JSON, which the bridge then
160
+ * reframes as Responses events. Use only for upstreams whose streaming response
161
+ * can omit or indefinitely delay the terminal event.
162
+ */
163
+ modelResponsesUpstreamStreaming?: Record<string, boolean>;
164
+ /**
165
+ * Registry-only client-facing item-id repair policy (#938), filled onto the
166
+ * runtime provider only when the user has no explicit policy (derive.ts);
167
+ * never seeded into saved config.
168
+ */
169
+ responsesItemIdRepair?: {
170
+ message?: string[];
171
+ reasoning?: string[];
172
+ repairMissingTerminalIds?: boolean;
173
+ repairInvalidIds?: boolean;
174
+ };
151
175
  /**
152
176
  * Responses-API resource path for providers whose route is not `/v1/responses`.
153
177
  * Unlike `modelWireDefaults` above, this IS seeded into saved config: it describes
@@ -161,6 +185,16 @@ export interface ProviderRegistryEntry {
161
185
  * replay miss are repaired rather than forwarded.
162
186
  */
163
187
  statelessResponses?: boolean;
188
+ /**
189
+ * Registry default for the provider's Responses `service_tier` support; see
190
+ * `OcxProviderConfig.supportsServiceTier`. Registry-only: backfilled (never
191
+ * overriding) at enrich/route time and deliberately NOT seeded into saved
192
+ * config, so an explicit user value stays distinguishable from the default
193
+ * (and the canonical openai seed comparison keeps its exact key set).
194
+ */
195
+ supportsServiceTier?: boolean;
196
+ /** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
197
+ preserveResponsesReasoningContent?: boolean;
164
198
  modelDiscovery?: ProviderModelDiscoverySpec;
165
199
  contextWindow?: number;
166
200
  modelContextWindows?: Record<string, number>;
@@ -172,6 +206,7 @@ export interface ProviderRegistryEntry {
172
206
  modelDefaultReasoningEfforts?: Record<string, string>;
173
207
  reasoningEffortMap?: Record<string, string>;
174
208
  modelReasoningEffortMap?: Record<string, Record<string, string>>;
209
+ reasoningWireFormat?: OcxProviderConfig["reasoningWireFormat"];
175
210
  noVisionModels?: string[];
176
211
  noReasoningModels?: string[];
177
212
  noTemperatureModels?: string[];
@@ -203,7 +238,7 @@ export type ProviderConfigSeed = Pick<
203
238
  "adapter" | "baseUrl" | "apiKeyTransport" | "responsesPath" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
204
239
  | "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
205
240
  | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
206
- | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
241
+ | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat"
207
242
  | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
208
243
  | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
209
244
  | "googleMode" | "project" | "location" | "headers"
@@ -214,7 +249,7 @@ export type ProviderConfigSeed = Pick<
214
249
  // 260710 context refresh: Tier-2 evidence in
215
250
  // devlog/_plan/260710_provider_hardening/001_research_frontier.md.
216
251
  const ANTHROPIC_MODELS = ["claude-fable-5", "claude-sonnet-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
217
- const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-haiku-4-5": 200_000 };
252
+ const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-opus-4-7": 1_000_000, "claude-opus-4-6": 1_000_000, "claude-sonnet-4-6": 1_000_000, "claude-haiku-4-5": 200_000 };
218
253
 
219
254
  const ZAI_GLM_52_MODELS = ["glm-5.2", "glm-5.2[1m]"];
220
255
  const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
@@ -328,14 +363,14 @@ const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
328
363
  // Evidence: https://help.aliyun.com/en/model-studio/token-plan-personal-overview
329
364
  // https://help.aliyun.com/en/model-studio/token-plan-quickstart
330
365
  const ALIBABA_TOKEN_PLAN_MODELS = [
331
- "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
366
+ "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
332
367
  "glm-5.2", "deepseek-v4-pro",
333
368
  ];
334
369
  const ALIBABA_TOKEN_PLAN_QWEN_MODELS = [
335
- "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
370
+ "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
336
371
  ];
337
372
  const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
338
- "qwen3.8-max-preview": ["text", "image"],
373
+ "qwen3.8-max": ["text", "image"],
339
374
  "qwen3.7-max": ["text", "image"],
340
375
  "qwen3.7-plus": ["text", "image"],
341
376
  "qwen3.6-flash": ["text", "image"],
@@ -348,14 +383,14 @@ const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
348
383
  // Evidence: https://www.alibabacloud.com/help/en/model-studio/token-plan-overview
349
384
  // https://qwencloud.com/pricing/token-plan (qwen3.8 metadata)
350
385
  const ALIBABA_INTL_TOKEN_PLAN_MODELS = [
351
- "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
386
+ "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
352
387
  "deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v3.2",
353
388
  "kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5",
354
389
  "glm-5.2", "glm-5.1", "glm-5",
355
390
  "MiniMax-M2.5",
356
391
  ];
357
392
  const ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS = [
358
- "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
393
+ "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
359
394
  ];
360
395
 
361
396
  // 260722 Tencent Cloud Coding Plan. The plan's model set is explicitly dynamic; these are the
@@ -426,7 +461,7 @@ const VOLCENGINE_PLAN_TEXT_ONLY_MODELS = [
426
461
  "doubao-seed-2.0-pro",
427
462
  ];
428
463
  const ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
429
- "qwen3.8-max-preview": ["text", "image"],
464
+ "qwen3.8-max": ["text", "image"],
430
465
  "qwen3.7-max": ["text", "image"],
431
466
  "qwen3.7-plus": ["text", "image"],
432
467
  "qwen3.6-plus": ["text", "image"],
@@ -496,6 +531,72 @@ const NVIDIA_NIM_KIMI_MODELS = [
496
531
  ...NVIDIA_NIM_KIMI_THINKING_MODELS,
497
532
  "moonshotai/kimi-k2-instruct", "moonshotai/kimi-k2-instruct-0905",
498
533
  ];
534
+ /**
535
+ * 260804 issue #956: NIM publishes no input-modality metadata on `/v1/models`, so the
536
+ * registry is the only source of truth for which models can see images.
537
+ *
538
+ * Two lists, both verified per-model against NVIDIA documentation on 2026-08-04
539
+ * (build.nvidia.com model pages and docs.api.nvidia.com/nim/reference/*). Evidence and
540
+ * the per-id audit: devlog/_plan/260804_stack7_service_vision/011_nim_id_audit.md.
541
+ *
542
+ * Read `noVisionModels` carefully — it lists models that CANNOT see images, which is
543
+ * what routes them through the proxy's vision sidecar (src/vision/index.ts) and makes the
544
+ * catalog advertise image input for them. Membership is wrong in BOTH directions:
545
+ * - a text-only model missing from it keeps issue #956 (images blocked or rejected);
546
+ * - a vision model wrongly IN it gets its image silently replaced by another model's
547
+ * text description — no error, worse answers, extra cost.
548
+ *
549
+ * A new NIM id must be classified DELIBERATELY against its NVIDIA page, never assumed
550
+ * from its name: `google/gemma-4-31b-it` carries no vision marker yet accepts images,
551
+ * `-vl` also appears on embedding/reranking models, and `google/codegemma-7b` is
552
+ * text-only while `google/codegemma-1.1-7b` has no current page at all. An unclassified
553
+ * id is intentionally left alone rather than defaulted, because NIM serves non-chat
554
+ * endpoints (embeddings, rerankers, guards, OCR) that reach the same code path.
555
+ */
556
+ const NVIDIA_NIM_VISION_MODELS = [
557
+ "meta/llama-3.2-11b-vision-instruct", "meta/llama-3.2-90b-vision-instruct",
558
+ "nvidia/llama-3.1-nemotron-nano-vl-8b-v1", "nvidia/nemotron-nano-12b-v2-vl",
559
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", "nvidia/cosmos3-nano-reasoner",
560
+ "nvidia/ising-calibration-1.5-31b", "nvidia/ising-calibration-1-35b-a3b",
561
+ "google/gemma-4-31b-it", "google/diffusiongemma-26b-a4b-it",
562
+ "minimaxai/minimax-m3", "moonshotai/kimi-k2.6", "moonshotai/kimi-k2.5",
563
+ "stepfun-ai/step-3.7-flash", "thinkingmachines/inkling",
564
+ "mistralai/mistral-medium-3.5-128b",
565
+ ];
566
+ /**
567
+ * The catalog advertises image input only for `noVisionModels` members, so a natively
568
+ * vision-capable model would otherwise be published as text-only and the Codex app would
569
+ * block attachments before the native path ever runs.
570
+ */
571
+ const NVIDIA_NIM_VISION_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
572
+ NVIDIA_NIM_VISION_MODELS.map(id => [id, ["text", "image"]]),
573
+ );
574
+ /**
575
+ * Text-only NIM chat models — 26 ids, each carrying an explicit `Input Modalities: Text`
576
+ * (or equivalent) on its NVIDIA page. PR #964 proposed ~64; six of those are natively
577
+ * image-capable and live in NVIDIA_NIM_VISION_MODELS above, and 32 more had no current
578
+ * NVIDIA page and were dropped rather than assumed.
579
+ *
580
+ * kimi-k2-thinking and kimi-k2-instruct are text-only while k2.5/k2.6 are not — vision
581
+ * and reasoning are independent axes, so all four stay in NVIDIA_NIM_KIMI_MODELS for
582
+ * reasoning suppression regardless of which list they appear in here.
583
+ */
584
+ const NVIDIA_NIM_NO_VISION_MODELS = [
585
+ "deepseek-ai/deepseek-v4-flash", "deepseek-ai/deepseek-v4-pro",
586
+ "google/codegemma-7b",
587
+ "meta/llama-3.1-70b-instruct", "meta/llama-3.1-8b-instruct",
588
+ "meta/llama-3.2-1b-instruct", "meta/llama-3.2-3b-instruct",
589
+ "meta/llama-3.3-70b-instruct", "meta/llama2-70b",
590
+ "mistralai/mistral-7b-instruct-v0.3", "mistralai/mistral-nemotron",
591
+ "moonshotai/kimi-k2-thinking", "moonshotai/kimi-k2-instruct",
592
+ "nvidia/llama-3.1-nemotron-nano-8b-v1", "nvidia/llama-3.1-nemotron-ultra-253b-v1",
593
+ "nvidia/llama-3.3-nemotron-super-49b-v1", "nvidia/llama-3.3-nemotron-super-49b-v1.5",
594
+ "nvidia/nemotron-3-nano-30b-a3b", "nvidia/nemotron-3-super-120b-a12b",
595
+ "nvidia/nemotron-3-ultra-550b-a55b", "nvidia/nemotron-mini-4b-instruct",
596
+ "nvidia/nvidia-nemotron-nano-9b-v2",
597
+ "openai/gpt-oss-120b", "openai/gpt-oss-20b",
598
+ "poolside/laguna-xs-2.1", "z-ai/glm-5.2",
599
+ ];
499
600
  const KIMI_CODING_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
500
601
  KIMI_CODING_MODELS.map(id => [id, id === "k3[1m]" ? KIMI_K3_1M_CONTEXT_WINDOW : KIMI_K3_STANDARD_CONTEXT_WINDOW]),
501
602
  );
@@ -566,6 +667,44 @@ const UMANS_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
566
667
  const UMANS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
567
668
  UMANS_MODELS.map(id => [id, UMANS_TEXT_ONLY_MODELS.includes(id) ? ["text"] : ["text", "image"]]),
568
669
  );
670
+ const CLINE_PASS_MODELS = [
671
+ "cline-pass/glm-5.2",
672
+ "cline-pass/kimi-k3",
673
+ "cline-pass/kimi-k2.7-code",
674
+ "cline-pass/kimi-k2.6",
675
+ "cline-pass/deepseek-v4-pro",
676
+ "cline-pass/deepseek-v4-flash",
677
+ "cline-pass/mimo-v2.5",
678
+ "cline-pass/mimo-v2.5-pro",
679
+ "cline-pass/minimax-m3",
680
+ "cline-pass/qwen3.7-max",
681
+ "cline-pass/qwen3.7-plus",
682
+ ];
683
+ const CLINE_PASS_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
684
+ "cline-pass/glm-5.2": 1_048_576,
685
+ "cline-pass/kimi-k3": 1_048_576,
686
+ "cline-pass/kimi-k2.7-code": 262_144,
687
+ "cline-pass/kimi-k2.6": 262_144,
688
+ "cline-pass/deepseek-v4-pro": 1_048_576,
689
+ "cline-pass/deepseek-v4-flash": 1_048_576,
690
+ "cline-pass/mimo-v2.5": 1_050_000,
691
+ "cline-pass/mimo-v2.5-pro": 1_050_000,
692
+ "cline-pass/minimax-m3": 1_048_576,
693
+ "cline-pass/qwen3.7-max": 1_000_000,
694
+ "cline-pass/qwen3.7-plus": 1_000_000,
695
+ };
696
+ const CLINE_PASS_IMAGE_MODELS = new Set([
697
+ "cline-pass/kimi-k3",
698
+ "cline-pass/kimi-k2.7-code",
699
+ "cline-pass/kimi-k2.6",
700
+ "cline-pass/mimo-v2.5",
701
+ "cline-pass/minimax-m3",
702
+ "cline-pass/qwen3.7-plus",
703
+ ]);
704
+ const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODELS.filter(id => !CLINE_PASS_IMAGE_MODELS.has(id));
705
+ const CLINE_PASS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
706
+ CLINE_PASS_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]),
707
+ );
569
708
 
570
709
  export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
571
710
  {
@@ -575,6 +714,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
575
714
  baseUrl: "https://chatgpt.com/backend-api/codex",
576
715
  authKind: "forward",
577
716
  codexAccountMode: "pool",
717
+ supportsServiceTier: true,
578
718
  featured: true,
579
719
  note: "Codex login account pool (default) or Direct main-account mode via codexAccountMode",
580
720
  },
@@ -745,6 +885,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
745
885
  adapter: "openai-responses",
746
886
  baseUrl: "https://api.openai.com/v1",
747
887
  authKind: "key",
888
+ supportsServiceTier: true,
748
889
  featured: true,
749
890
  dashboardUrl: "https://platform.openai.com/api-keys",
750
891
  defaultModel: "gpt-5.5",
@@ -869,6 +1010,56 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
869
1010
  preserveReasoningContentModels: NEURALWATT_REASONING_HISTORY_MODELS,
870
1011
  },
871
1012
  { id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter", models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS], modelContextWindows: { "anthropic/claude-sonnet-5": 1_000_000, ...OPENROUTER_GPT56_CONTEXT_WINDOWS } },
1013
+ {
1014
+ // Primary sources checked 2026-08-02:
1015
+ // - docs.cline.bot/getting-started/clinepass publishes this exact catalog and explicitly
1016
+ // authorizes using the full slugs through Cline's external API.
1017
+ // - docs.cline.bot/api/chat-completions and /api/errors define the endpoint, reasoning delta,
1018
+ // and choice-scoped mid-stream error contract.
1019
+ // - Cline's official catalog source resolves per-model capabilities through OpenRouter data;
1020
+ // the static context/modality snapshot below was cross-checked against that catalog.
1021
+ // - cline.bot/tos identifies Cline Bot Inc. as the operator. Maintenance owner: @lidge-jun.
1022
+ id: "cline-pass",
1023
+ label: "ClinePass",
1024
+ adapter: "openai-chat",
1025
+ baseUrl: "https://api.cline.bot/api/v1",
1026
+ authKind: "key",
1027
+ dashboardUrl: "https://app.cline.bot",
1028
+ defaultModel: "cline-pass/kimi-k3",
1029
+ models: CLINE_PASS_MODELS,
1030
+ modelContextWindows: CLINE_PASS_MODEL_CONTEXT_WINDOWS,
1031
+ modelInputModalities: CLINE_PASS_MODEL_INPUT_MODALITIES,
1032
+ noVisionModels: CLINE_PASS_TEXT_ONLY_MODELS,
1033
+ // Only low and the `reasoning: { enabled, effort }` request shape have been accepted by a live
1034
+ // ClinePass request. Neither wire detail is currently documented, so clamp higher Codex
1035
+ // requests to the verified tier until the gateway documents or is live-probed more broadly.
1036
+ reasoningEfforts: ["low"],
1037
+ reasoningWireFormat: "gateway-object",
1038
+ preserveCustomDestination: true,
1039
+ note: "ClinePass subscription API. Uses a Cline API key and the full cline-pass/<model> upstream slug; quota is shared across the account's rolling 5-hour, weekly, and monthly limits.",
1040
+ },
1041
+ // Cline API (usage-billing): OpenAI-compatible Chat Completions. Model IDs follow the
1042
+ // OpenRouter-style `provider/model` convention. Live /models discovery is key-gated (401
1043
+ // without auth), so the static seed is the cold-start fallback. Evidence: docs.cline.bot/api/*.
1044
+ {
1045
+ id: "cline",
1046
+ label: "Cline",
1047
+ adapter: "openai-chat",
1048
+ baseUrl: "https://api.cline.bot/api/v1",
1049
+ authKind: "key",
1050
+ dashboardUrl: "https://app.cline.bot",
1051
+ liveModels: true,
1052
+ defaultModel: "anthropic/claude-sonnet-4-6",
1053
+ models: [
1054
+ "anthropic/claude-sonnet-4-6",
1055
+ "openai/gpt-4o",
1056
+ "google/gemini-2.5-pro",
1057
+ "deepseek/deepseek-chat",
1058
+ "minimax/minimax-m2.5",
1059
+ ],
1060
+ preserveCustomDestination: true,
1061
+ note: "Cline usage-billing API: one key, 100+ models, OpenRouter-style ids. Promotional free models are IDE/CLI-only per Cline docs; minimax/minimax-m2.5 is the documented API free experimentation model.",
1062
+ },
872
1063
  {
873
1064
  // OrcaRouter: OpenAI-compatible adaptive router (api.orcarouter.ai). Model ids are
874
1065
  // vendor-namespaced (`<vendor>/<model>`) and pass through to the upstream as-is.
@@ -960,11 +1151,29 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
960
1151
  // for no gain.
961
1152
  "deepseek-v4-flash": { wire: "openai-responses", inbound: ["responses"] },
962
1153
  },
1154
+ // DeepSeek's Codex Responses stream can deliver output without closing on the
1155
+ // terminal event. Keep Codex on WebSocket, but use the provider's bounded JSON
1156
+ // response upstream so the bridge can synthesize a complete WS event sequence.
1157
+ modelResponsesUpstreamStreaming: { "deepseek-v4-flash": false },
1158
+ // DeepSeek's Responses route emits bare UUID item ids, which leave Codex
1159
+ // clients stuck on an uncommitted turn (#938). Client-facing only — raw
1160
+ // continuation snapshots keep the upstream ids.
1161
+ responsesItemIdRepair: { repairInvalidIds: true, repairMissingTerminalIds: true },
963
1162
  // DeepSeek's Responses route is `POST /responses` with no `/v1` segment. Without
964
1163
  // this the passthrough adapter falls back to its legacy `/v1/responses`
965
1164
  // construction and the wire above can never route.
966
1165
  // Evidence: https://api-docs.deepseek.com/api/create-response/
967
1166
  responsesPath: "/responses",
1167
+ // DeepSeek's Responses reference does not list `service_tier`; unsupported
1168
+ // parameters are documented as silently ignored, but the fail-closed policy
1169
+ // strips the field rather than forwarding a knob the upstream never asked for.
1170
+ supportsServiceTier: false,
1171
+ // DeepSeek's Responses compatibility guide accepts plaintext reasoning items and
1172
+ // merges them into the adjacent assistant message, so replayed reasoning must
1173
+ // not be blanked the way the ChatGPT backend requires. (Whether the Responses
1174
+ // route REQUIRES replay on tool-call continuations is an inference from the
1175
+ // Chat Thinking-Mode docs, not a confirmed Responses contract.)
1176
+ preserveResponsesReasoningContent: true,
968
1177
  // "The API is stateless: responses and conversations are not stored on the
969
1178
  // server." https://api-docs.deepseek.com/api/create-response/
970
1179
  statelessResponses: true,
@@ -1045,6 +1254,32 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1045
1254
  },
1046
1255
  note: "Shared Model APIs only (personal API key, or team key with Call Model APIs access); dedicated Truss predict endpoints are outside this preset.",
1047
1256
  },
1257
+ {
1258
+ id: "commandcode",
1259
+ label: "Command Code",
1260
+ adapter: "openai-chat",
1261
+ baseUrl: "https://api.commandcode.ai/provider/v1",
1262
+ authKind: "key",
1263
+ dashboardUrl: "https://commandcode.ai/studio/",
1264
+ liveModels: true,
1265
+ preserveCustomDestination: true,
1266
+ defaultModel: "deepseek/deepseek-v4-flash",
1267
+ // The default is also the cold-start seed: live discovery failure must not empty the catalog
1268
+ // for a freshly configured provider with no stale cache (issue #308 pattern).
1269
+ models: ["deepseek/deepseek-v4-flash"],
1270
+ // The public model catalog is unauthenticated, so a Bearer probe cannot prove key validity.
1271
+ apiKeyValidation: "unknown",
1272
+ // The public catalog reports ids/context windows only; no trustworthy reasoning contract.
1273
+ reasoningEfforts: [],
1274
+ modelDiscovery: {
1275
+ path: "models",
1276
+ maxResponseBytes: 256 * 1024,
1277
+ maxModels: 256,
1278
+ },
1279
+ // Verified 2026-08-03: public /provider/v1/models returns 51 rows; /chat/completions returns
1280
+ // 401 UNAUTHORIZED without a Bearer key. Primary source: https://commandcode.ai/docs/provider.
1281
+ note: "Command Code Provider API (OpenAI-compatible); API access requires the Provider plan. CLI auth bridging for Go/Pro subscriptions is not yet available. Docs: https://commandcode.ai/docs/provider.",
1282
+ },
1048
1283
  // FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
1049
1284
  { id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" },
1050
1285
  { id: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" },
@@ -1081,6 +1316,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1081
1316
  // Free pricing, but an API key is still required (free key from build.nvidia.com).
1082
1317
  freeTier: true,
1083
1318
  parallelToolCalls: false,
1319
+ // 260804 issue #956: NIM exposes no input modalities, so vision capability is
1320
+ // classified here. Both lists are verified per-model; unlisted ids stay unclassified
1321
+ // by design (see the comment on NVIDIA_NIM_VISION_MODELS).
1322
+ noVisionModels: NVIDIA_NIM_NO_VISION_MODELS,
1323
+ modelInputModalities: NVIDIA_NIM_VISION_INPUT_MODALITIES,
1084
1324
  noReasoningModels: NVIDIA_NIM_KIMI_MODELS,
1085
1325
  modelReasoningEfforts: Object.fromEntries(NVIDIA_NIM_KIMI_MODELS.map(id => [id, []])),
1086
1326
  preserveReasoningContentModels: NVIDIA_NIM_KIMI_THINKING_MODELS,
@@ -1245,6 +1485,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1245
1485
  responsesPath: "/responses",
1246
1486
  adapter: "openai-responses",
1247
1487
  authKind: "key",
1488
+ // Ark's plan route does not document `service_tier`; fail closed like DeepSeek.
1489
+ supportsServiceTier: false,
1248
1490
  preserveCustomDestination: true,
1249
1491
  dashboardUrl: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview",
1250
1492
  defaultModel: "deepseek-v4-pro",
@@ -1265,13 +1507,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1265
1507
  adapter: "openai-chat",
1266
1508
  authKind: "key",
1267
1509
  dashboardUrl: "https://bailian.console.aliyun.com/cn-beijing?tab=plan",
1268
- defaultModel: "qwen3.8-max-preview",
1510
+ defaultModel: "qwen3.8-max",
1269
1511
  models: ALIBABA_TOKEN_PLAN_MODELS,
1270
1512
  liveModels: false,
1271
1513
  note: "Token Plan Personal Edition · China (Beijing)",
1272
1514
  modelInputModalities: ALIBABA_TOKEN_PLAN_INPUT_MODALITIES,
1273
1515
  modelContextWindows: {
1274
- "qwen3.8-max-preview": 983_616, "qwen3.7-max": 1_000_000, "qwen3.7-plus": 1_000_000,
1516
+ "qwen3.8-max": 983_616, "qwen3.7-max": 1_000_000, "qwen3.7-plus": 1_000_000,
1275
1517
  "qwen3.6-flash": 1_000_000, "glm-5.2": 1_000_000, "deepseek-v4-pro": 1_000_000,
1276
1518
  },
1277
1519
  modelReasoningEfforts: {
@@ -1281,7 +1523,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1281
1523
  },
1282
1524
  modelReasoningEffortMap: { "deepseek-v4-pro": DEEPSEEK_THINKING_REASONING_MAP },
1283
1525
  thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS,
1284
- preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash"],
1526
+ preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash"],
1285
1527
  noVisionModels: ["glm-5.2", "deepseek-v4-pro"],
1286
1528
  },
1287
1529
  {
@@ -1300,7 +1542,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1300
1542
  metadataModelIdNormalize: "case-insensitive",
1301
1543
  modelInputModalities: ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES,
1302
1544
  modelContextWindows: {
1303
- "qwen3.8-max-preview": 983_616,
1545
+ "qwen3.8-max": 983_616,
1304
1546
  "qwen3.7-max": 1_000_000, "qwen3.7-plus": 1_000_000, "qwen3.6-plus": 1_000_000, "qwen3.6-flash": 1_000_000,
1305
1547
  "deepseek-v4-pro": 1_000_000, "deepseek-v4-flash": 1_000_000, "deepseek-v3.2": 131_072,
1306
1548
  "kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 262_144,
@@ -1309,7 +1551,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1309
1551
  },
1310
1552
  modelReasoningEfforts: {
1311
1553
  ...Object.fromEntries(ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
1312
- "qwen3.8-max-preview": ["low", "high", "xhigh"],
1554
+ "qwen3.8-max": ["low", "high", "xhigh"],
1313
1555
  "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
1314
1556
  "deepseek-v4-pro": DEEPSEEK_THINKING_EFFORTS,
1315
1557
  "deepseek-v4-flash": DEEPSEEK_THINKING_EFFORTS,
@@ -1319,10 +1561,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1319
1561
  "deepseek-v4-flash": DEEPSEEK_THINKING_REASONING_MAP,
1320
1562
  },
1321
1563
  thinkingBudgetModels: ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS,
1322
- preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash", "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash"],
1564
+ preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash", "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash"],
1323
1565
  noVisionModels: ["deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v3.2", "glm-5.2", "glm-5.1", "glm-5", "MiniMax-M2.5"],
1324
1566
  noReasoningModels: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "deepseek-v3.2", "glm-5.1", "glm-5", "MiniMax-M2.5"],
1325
- modelDefaultReasoningEfforts: { "qwen3.8-max-preview": "xhigh" },
1567
+ modelDefaultReasoningEfforts: { "qwen3.8-max": "xhigh" },
1326
1568
  },
1327
1569
  // NEEDS_HUMAN 2026-07-10: kept for config compatibility, but this is a dashboard URL,
1328
1570
  // no /models endpoint is documented, and tools are silently ignored upstream per docs.parallel.ai.
@@ -1476,8 +1718,23 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1476
1718
  featured: false,
1477
1719
  dashboardUrl: "https://github.com/settings/copilot",
1478
1720
  liveModels: true,
1479
- models: ["gpt-4o", "gpt-4.1", "gpt-4.1-mini", "claude-sonnet-4", "gemini-2.5-pro"],
1721
+ models: ["gpt-4o", "gpt-4.1", "gpt-4.1-mini", "claude-sonnet-4", "gemini-2.5-pro", "gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
1480
1722
  defaultModel: "gpt-4o",
1723
+ // Copilot fronts a mixed-wire catalog: these models reject /chat/completions for
1724
+ // real Codex-agent traffic (function tools + reasoning), so every inbound wire
1725
+ // rides Responses. Evidence: issue #748 field runs, pi.dev/models/github-copilot/*
1726
+ // wire declarations, BerriAI/litellm#23332 (gpt-5.4), JetBrains LLM-29711
1727
+ // (gpt-5.6-sol). gpt-5.4-nano is deliberately absent — it has no field report; a
1728
+ // user can opt it in with an explicit modelAdapters entry, which always wins.
1729
+ modelWireDefaults: {
1730
+ "gpt-5.3-codex": "openai-responses",
1731
+ "gpt-5.4": "openai-responses",
1732
+ "gpt-5.4-mini": "openai-responses",
1733
+ "gpt-5.5": "openai-responses",
1734
+ "gpt-5.6-luna": "openai-responses",
1735
+ "gpt-5.6-sol": "openai-responses",
1736
+ "gpt-5.6-terra": "openai-responses",
1737
+ },
1481
1738
  note: "Experimental unofficial Copilot bridge. Logs in via GitHub device flow using the public VS Code OAuth client id, then exchanges for a short-lived Copilot API token (copilot_internal). Requires an active Copilot subscription. GitHub may tighten or revoke this path; do not send confidential material you would not paste into Copilot Chat.",
1482
1739
  },
1483
1740
  // FREEZE 2026-07-10: no public OpenAI-compatible endpoint is documented. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
@@ -1573,6 +1830,17 @@ export function providerModelWireDefault(
1573
1830
  return wire !== undefined && allowedWires.has(wire) ? wire : undefined;
1574
1831
  }
1575
1832
 
1833
+ /** Resolve a registry-only upstream-streaming compatibility hint for Responses turns. */
1834
+ export function providerModelResponsesUpstreamStreaming(
1835
+ id: string,
1836
+ provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
1837
+ modelId: string,
1838
+ ): boolean | undefined {
1839
+ const entry = getProviderRegistryEntry(id);
1840
+ if (!entry?.modelResponsesUpstreamStreaming || !providerMatchesRegistryTransport(id, provider)) return undefined;
1841
+ return entry.modelResponsesUpstreamStreaming[modelId.trim().toLowerCase()];
1842
+ }
1843
+
1576
1844
  /**
1577
1845
  * Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
1578
1846
  * `codexAccountMode` on the provider config wins and a missing/invalid value defaults to
@@ -53,7 +53,7 @@ function withoutUserOverridden(
53
53
 
54
54
  function withGeneratedRequestId(
55
55
  init: RequestInit | undefined,
56
- configuredRequestId: string | undefined,
56
+ pinnedRequestId: string,
57
57
  stableHeaders: Readonly<Record<string, string>>,
58
58
  ): RequestInit {
59
59
  const headers = new Headers(init?.headers);
@@ -63,7 +63,7 @@ function withGeneratedRequestId(
63
63
  if (!headers.has(XAI_GROK_COMPATIBILITY.headers.requestId)) {
64
64
  headers.set(
65
65
  XAI_GROK_COMPATIBILITY.headers.requestId,
66
- configuredRequestId ?? randomUUID(),
66
+ pinnedRequestId,
67
67
  );
68
68
  }
69
69
  return { ...init, headers };
@@ -83,7 +83,9 @@ export function deriveXaiConvId(promptCacheKey: string): string {
83
83
 
84
84
  /**
85
85
  * Resolve xAI's runtime transport without mutating persisted config. Conversation/session
86
- * affinity is stable for this resolved transport; request identity is generated per fetch.
86
+ * affinity is stable for this resolved transport; request identity is pinned per resolved
87
+ * transport (= per logical request until key rotation), so same-target replays and transient
88
+ * retries carry the same id while a rotated key gets a fresh one.
87
89
  * Agent, deployment, model-override, turn, mode, and user identity headers are intentionally
88
90
  * omitted because opencodex has no truthful values for the official fields.
89
91
  */
@@ -128,9 +130,14 @@ export function resolveProviderTransport(
128
130
  provider.headers,
129
131
  XAI_GROK_COMPATIBILITY.headers.requestId,
130
132
  );
133
+ // Pin the request id per resolved transport (= per logical request until key rotation):
134
+ // same-target 429 replays must carry the SAME x-grok-req-id as the original dispatch, and
135
+ // transient retries reuse one id so the upstream can dedupe them. A rotated key resolves a
136
+ // fresh transport, which gets its own id.
137
+ const requestId = configuredRequestId ?? randomUUID();
131
138
  const baseFetch = provider.fetch ?? globalThis.fetch;
132
139
  const attemptFetch = ((input, init) =>
133
- baseFetch(input, withGeneratedRequestId(init, configuredRequestId, stableHeaders))) as typeof globalThis.fetch;
140
+ baseFetch(input, withGeneratedRequestId(init, requestId, stableHeaders))) as typeof globalThis.fetch;
134
141
 
135
142
  return {
136
143
  ...provider,
@@ -105,7 +105,14 @@ export function buildCompactV1Output(userMessages: string[], summary: string): R
105
105
  remaining -= msg.length;
106
106
  } else {
107
107
  // Budget partially covers this older message: keep its tail (most recent context) and stop.
108
- selected.push(msg.slice(msg.length - remaining));
108
+ let tailStart = msg.length - remaining;
109
+ // Never start the retained tail on a lone LOW surrogate: the pair's
110
+ // other half would be lost and encoding substitutes U+FFFD.
111
+ if (tailStart > 0 && tailStart < msg.length) {
112
+ const first = msg.charCodeAt(tailStart);
113
+ if (first >= 0xdc00 && first <= 0xdfff) tailStart += 1;
114
+ }
115
+ selected.push(msg.slice(tailStart));
109
116
  break;
110
117
  }
111
118
  }
@@ -0,0 +1,9 @@
1
+ /** Hosted tools rejected by specific native model slugs. */
2
+ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string) => boolean; tools: ReadonlySet<string> }> = [
3
+ { match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
4
+ ];
5
+
6
+ /** True when forwarding this hosted tool to the model would be rejected upstream. */
7
+ export function isHostedToolUnsupportedForModel(modelId: string, tool: string): boolean {
8
+ return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId) && entry.tools.has(tool));
9
+ }
@@ -263,6 +263,34 @@ function findToolById(messages: OcxMessage[], callId: string): { name: string; n
263
263
  return { name: "" };
264
264
  }
265
265
 
266
+ /**
267
+ * Attach pending reasoning to the assistant turn that owns the given call id.
268
+ * Reconstructed histories (resume/retry/synthetic) can order a `reasoning`
269
+ * item AFTER the `function_call` it belongs to; without this, the pending
270
+ * buffer is cleared at the tool output and the turn serializes without
271
+ * `reasoning_content`, which DeepSeek thinking mode rejects with HTTP 400
272
+ * (issue #950).
273
+ */
274
+ function attachPendingReasoningToCallOwner(
275
+ messages: OcxMessage[],
276
+ callId: string,
277
+ pendingReasoning: Array<{ part: OcxThinkingContent; envelopeSigned: boolean }>,
278
+ ): void {
279
+ if (pendingReasoning.length === 0 || !callId) return;
280
+ for (let i = messages.length - 1; i >= 0; i--) {
281
+ const m = messages[i];
282
+ if (m.role !== "assistant") continue;
283
+ for (const part of m.content) {
284
+ if (part.type === "toolCall" && part.id === callId) {
285
+ // Prepend so thinking still precedes tool_use for adapters that require
286
+ // that ordering (Anthropic-style replay).
287
+ m.content = [...pendingReasoning.map(entry => entry.part), ...m.content];
288
+ return;
289
+ }
290
+ }
291
+ }
292
+ }
293
+
266
294
  const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
267
295
 
268
296
  export function parseRequest(body: unknown): OcxParsedRequest {
@@ -416,6 +444,18 @@ export function parseRequest(body: unknown): OcxParsedRequest {
416
444
  : null;
417
445
  const thinkingText = envelope?.txt || text;
418
446
 
447
+ // Kiro reasoning round-trip: a krc-only item carries nothing renderable — it is provider
448
+ // state for the assistant turn that ALREADY closed, because Kiro emits its
449
+ // reasoningContentEvent at the END of a turn (after content AND tool calls, verified
450
+ // against kiro-cli 2.14.1/2.16.0). Folding it into the FOLLOWING turn like ordinary
451
+ // reasoning would attach turn N's blob to turn N+1, so attach it backwards instead. With
452
+ // no assistant turn to own it the blob is dropped rather than mis-paired.
453
+ if (envelope?.krc && thinkingText.length === 0) {
454
+ const previous = messages[messages.length - 1];
455
+ if (previous?.role === "assistant") previous.kiroRedactedReasoning = envelope.krc;
456
+ continue;
457
+ }
458
+
419
459
  // Native/non-ocxr1 encrypted-only reasoning is opaque here. Do not create a detached
420
460
  // assistant turn or invent replayable plaintext/signatures from the encrypted payload.
421
461
  if (thinkingText.length > 0) {
@@ -545,8 +585,9 @@ export function parseRequest(body: unknown): OcxParsedRequest {
545
585
  }
546
586
 
547
587
  if (effectiveType === "function_call_output") {
548
- pendingReasoning.length = 0;
549
588
  const output = item as { call_id: string; output?: string | unknown[] };
589
+ attachPendingReasoningToCallOwner(messages, output.call_id, pendingReasoning);
590
+ pendingReasoning.length = 0;
550
591
  const toolInfo = findToolById(messages, output.call_id);
551
592
  messages.push({
552
593
  role: "toolResult", toolCallId: output.call_id,
@@ -558,8 +599,9 @@ export function parseRequest(body: unknown): OcxParsedRequest {
558
599
  }
559
600
 
560
601
  if (effectiveType === "custom_tool_call_output") {
561
- pendingReasoning.length = 0;
562
602
  const output = item as { call_id: string; output: string | unknown[] };
603
+ attachPendingReasoningToCallOwner(messages, output.call_id, pendingReasoning);
604
+ pendingReasoning.length = 0;
563
605
  const toolInfo = findToolById(messages, output.call_id);
564
606
  messages.push({
565
607
  role: "toolResult", toolCallId: output.call_id,