@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -14,9 +14,13 @@
14
14
  // This migration is deliberately NOT general reconciliation. It rewrites exactly
15
15
  // one thing: an id this file declares retired, on a provider that still carries
16
16
  // the registry's transport, and only when the registry currently seeds the
17
- // replacement. Everything else in the row is left alone.
17
+ // replacement — and it skips a field where the retired entry is registry residue
18
+ // with nothing left to carry forward (see `isRegistryResidue`, issue #5066).
19
+ // Everything else in the row is left alone.
18
20
 
19
21
  import { PROVIDER_REGISTRY } from "./registry";
22
+ import { MODEL_KEYED_RECORDS, MODEL_ID_LISTS, MODEL_ID_SCALARS, MODEL_NESTED_RECORDS } from "./model-rename-fields";
23
+ import { providerConfigSeed } from "./derive";
20
24
  import type { OcxConfig, OcxProviderConfig } from "../types";
21
25
 
22
26
  export interface ModelRename {
@@ -42,6 +46,52 @@ export interface ModelRename {
42
46
  * but lose the reasoning picker entirely.
43
47
  */
44
48
  dropReasoningEffortMap?: boolean;
49
+ /**
50
+ * Remove both the retired id and its replacement from `noReasoningModels`.
51
+ *
52
+ * Use this only when the rename also marks a capability change: carrying the old
53
+ * no-reasoning classification onto a newly adjustable alias would keep the picker
54
+ * disabled after the model id itself was repaired.
55
+ */
56
+ dropNoReasoningModels?: boolean;
57
+ /** Refresh exact registry defaults already saved under the replacement id. */
58
+ targetSeedRefresh?: {
59
+ contextWindow?: { from: number; to: number };
60
+ reasoning?: {
61
+ fromEfforts: readonly string[];
62
+ toEfforts: readonly string[];
63
+ defaultEffort: string;
64
+ effortMap: Readonly<Record<string, string>>;
65
+ };
66
+ };
67
+ }
68
+
69
+ const KIMI_K28_ALIAS = "kimi-for-coding";
70
+ const KIMI_RETIRED_CODING_IDS = [
71
+ "kimi-k2.7-code",
72
+ "kimi-k2.7-code-highspeed",
73
+ "kimi-k2.6",
74
+ "kimi-k2.5",
75
+ ] as const;
76
+ const KIMI_K28_TARGET_SEED_REFRESH: NonNullable<ModelRename["targetSeedRefresh"]> = {
77
+ contextWindow: { from: 262_144, to: 1_048_576 },
78
+ reasoning: {
79
+ fromEfforts: [],
80
+ toEfforts: ["low", "high", "max"],
81
+ defaultEffort: "max",
82
+ effortMap: { none: "none", low: "low", medium: "high", high: "high", xhigh: "max", max: "max" },
83
+ },
84
+ };
85
+
86
+ function kimiCodingRenames(provider: "kimi" | "kimi-code", endpoint: string): ModelRename[] {
87
+ return KIMI_RETIRED_CODING_IDS.map(from => ({
88
+ provider,
89
+ from,
90
+ to: KIMI_K28_ALIAS,
91
+ reason: `Moonshot retired the k2.x coding ids from the ${endpoint}; kimi-for-coding is the stable alias the endpoint still serves`,
92
+ dropNoReasoningModels: true,
93
+ targetSeedRefresh: KIMI_K28_TARGET_SEED_REFRESH,
94
+ }));
45
95
  }
46
96
 
47
97
  /**
@@ -62,6 +112,14 @@ export const MODEL_RENAMES: readonly ModelRename[] = [
62
112
  to: "qwen3.8-max",
63
113
  reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
64
114
  },
115
+ // Kimi coding renames. Moonshot retired the k2.x ids from the subscription/coding
116
+ // endpoint when K2.8 Preview shipped (live /coding/v1/models lists only
117
+ // kimi-for-coding[-highspeed], k3, k3-256k); kimi-for-coding is the stable alias the
118
+ // endpoint still serves and currently routes to K2.8 Preview. The registry picker no
119
+ // longer seeds the retired ids, so a saved defaultModel naming one is a dead selection
120
+ // rather than a merely outdated one.
121
+ ...kimiCodingRenames("kimi", "subscription endpoint after K2.8 Preview shipped"),
122
+ ...kimiCodingRenames("kimi-code", "coding endpoint after K2.8 Preview shipped"),
65
123
  // Antigravity Flash generations. Google takes the previous Flash model off Cloud Code
66
124
  // Assist almost immediately when the next ships, so a saved 3.6 (or older 3.5) id is a
67
125
  // dead selection rather than a merely outdated one. Routing already redirects these ids
@@ -80,40 +138,6 @@ export const MODEL_RENAMES: readonly ModelRename[] = [
80
138
  })),
81
139
  ];
82
140
 
83
- /** Provider fields that key metadata by model id. */
84
- const MODEL_KEYED_RECORDS = [
85
- "modelContextWindows",
86
- "modelMaxOutputTokens",
87
- "modelInputModalities",
88
- "modelReasoningEfforts",
89
- "modelSuppressSyntheticMax",
90
- "modelDefaultReasoningEfforts",
91
- "modelReasoningEffortMap",
92
- ] as const;
93
-
94
- /** Provider fields that are flat lists of model ids. */
95
- const MODEL_ID_LISTS = [
96
- "models",
97
- // A retired id left here is worse than a stale label: `filterCatalogModels` treats
98
- // `selectedModels` as an exact-match allowlist, so a user who allowlisted only the
99
- // retired model gets NO replacement row at all — the model silently vanishes from
100
- // their catalog instead of being renamed. OAuth reconciliation does not cover this
101
- // field, so the rename has to.
102
- "selectedModels",
103
- // Same reasoning as `selectedModels`: a retired id pinned here would be resurrected as a
104
- // ghost row on every discovery instead of following the rename (#1690).
105
- "retainModels",
106
- "noVisionModels",
107
- "noReasoningModels",
108
- "noTemperatureModels",
109
- "noTopPModels",
110
- "noPenaltyModels",
111
- "autoToolChoiceOnlyModels",
112
- "preserveReasoningContentModels",
113
- "thinkingBudgetModels",
114
- "directReasoningEffortModels",
115
- ] as const;
116
-
117
141
  function renameInList(value: unknown, from: string, to: string): string[] | null {
118
142
  if (!Array.isArray(value) || !value.includes(from)) return null;
119
143
  const seen = new Set<string>();
@@ -130,6 +154,18 @@ function renameInList(value: unknown, from: string, to: string): string[] | null
130
154
  return next;
131
155
  }
132
156
 
157
+ function dropRenamedIdsFromList(
158
+ value: unknown,
159
+ from: string,
160
+ to: string,
161
+ dropStaleTarget: boolean,
162
+ ): string[] | null {
163
+ if (!Array.isArray(value)) return null;
164
+ const hasRetired = value.includes(from);
165
+ if (!hasRetired && !(dropStaleTarget && value.includes(to))) return null;
166
+ return value.filter(entry => typeof entry === "string" && entry !== from && entry !== to);
167
+ }
168
+
133
169
  function renameInRecord(value: unknown, from: string, to: string): Record<string, unknown> | null {
134
170
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
135
171
  const record = value as Record<string, unknown>;
@@ -157,6 +193,42 @@ function dropFromRecord(value: unknown, from: string): Record<string, unknown> |
157
193
  return next;
158
194
  }
159
195
 
196
+ function sameStringArray(value: unknown, expected: readonly string[]): boolean {
197
+ return Array.isArray(value)
198
+ && value.length === expected.length
199
+ && value.every((entry, index) => entry === expected[index]);
200
+ }
201
+
202
+ function targetReasoningMatchesStaleSeed(row: Record<string, unknown>, rename: ModelRename): boolean {
203
+ const reasoning = rename.targetSeedRefresh?.reasoning;
204
+ const efforts = row.modelReasoningEfforts as Record<string, unknown> | undefined;
205
+ return !!reasoning && !!efforts && sameStringArray(efforts[rename.to], reasoning.fromEfforts);
206
+ }
207
+
208
+ /** Refresh only exact defaults emitted by the previous registry; preserve user overrides. */
209
+ function refreshTargetSeed(row: Record<string, unknown>, rename: ModelRename): boolean {
210
+ const refresh = rename.targetSeedRefresh;
211
+ if (!refresh) return false;
212
+ let changed = false;
213
+
214
+ const windows = row.modelContextWindows as Record<string, unknown> | undefined;
215
+ if (windows && refresh.contextWindow && windows[rename.to] === refresh.contextWindow.from) {
216
+ windows[rename.to] = refresh.contextWindow.to;
217
+ changed = true;
218
+ }
219
+
220
+ const reasoning = refresh.reasoning;
221
+ const efforts = row.modelReasoningEfforts as Record<string, unknown> | undefined;
222
+ if (!reasoning || !efforts || !sameStringArray(efforts[rename.to], reasoning.fromEfforts)) return changed;
223
+ efforts[rename.to] = [...reasoning.toEfforts];
224
+
225
+ const defaults = (row.modelDefaultReasoningEfforts ??= {}) as Record<string, unknown>;
226
+ if (!(rename.to in defaults)) defaults[rename.to] = reasoning.defaultEffort;
227
+ const maps = (row.modelReasoningEffortMap ??= {}) as Record<string, unknown>;
228
+ if (!(rename.to in maps)) maps[rename.to] = { ...reasoning.effortMap };
229
+ return true;
230
+ }
231
+
160
232
  /**
161
233
  * `provider/model` rows in the top-level `disabledModels` list.
162
234
  *
@@ -194,6 +266,54 @@ function registrySeedsTarget(rename: ModelRename): boolean {
194
266
  return !!entry?.models?.includes(rename.to);
195
267
  }
196
268
 
269
+ /** The row the registry contributes to a saved config, or `undefined` when it dropped the id. */
270
+ function registrySeed(provider: string): Record<string, unknown> | undefined {
271
+ const entry = PROVIDER_REGISTRY.find(row => row.id === provider);
272
+ return entry ? (providerConfigSeed(entry) as unknown as Record<string, unknown>) : undefined;
273
+ }
274
+
275
+ /** Whether a saved list or model-keyed record names this id at all. */
276
+ function publishes(container: unknown, id: string): boolean {
277
+ if (Array.isArray(container)) return container.includes(id);
278
+ if (!container || typeof container !== "object") return false;
279
+ return id in (container as Record<string, unknown>);
280
+ }
281
+
282
+ /**
283
+ * Whether this field’s retired entry is registry residue rather than something to repair.
284
+ *
285
+ * Two things have to be true. The registry’s own seed — `providerConfigSeed`, the row both the
286
+ * OAuth preset (`deriveOAuthProviderConfig`) and `enrichProviderFromRegistry` contribute — must
287
+ * still publish the retired id in this field, so the entry arrives in the config on its own. And
288
+ * the saved field must already carry the supported id, so the rename would only DELETE a key,
289
+ * with nothing to carry forward.
290
+ *
291
+ * Under those two conditions the rewrite is worse than a no-op, because it never converges.
292
+ * `startServer` runs this migration and then `reconcileOAuthProviders`, and
293
+ * `applyOAuthPresetCatalog` copies the preset’s record over the saved one whenever the two
294
+ * differ. The deletion is undone inside the same boot, the config is written twice, and the user
295
+ * is told about a rename that never sticks on every single start. Issue #5066 is exactly that:
296
+ * `ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` derives a window for every compatibility alias, so
297
+ * `modelContextWindows` holds a key for all nine retired Flash ids beside the live ones — the
298
+ * nine `[model-rename-migration]` lines the reporter saw at every `ocx start`, none of which
299
+ * required them to have selected any of those nine models.
300
+ *
301
+ * Those registry entries are deliberate and stay: a request that still names `gemini-3.6-flash`
302
+ * routes to 3.7 and needs a context window under the id it asked for. The second condition is
303
+ * what keeps this narrow. A row that saved `{ "gemini-3.6-flash": 1048576 }` and nothing else
304
+ * still gets the value carried onto the supported id, because dropping it there would blank the
305
+ * record instead of moving it (`tests/adapters/google/gemini-37-flash-migration.test.ts`).
306
+ */
307
+ function isRegistryResidue(
308
+ seed: Record<string, unknown> | undefined,
309
+ field: string,
310
+ saved: unknown,
311
+ rename: ModelRename,
312
+ ): boolean {
313
+ if (!seed) return false;
314
+ return publishes(seed[field], rename.from) && publishes(saved, rename.to);
315
+ }
316
+
197
317
  export interface ModelRenameProjection {
198
318
  config: OcxConfig;
199
319
  changed: boolean;
@@ -226,14 +346,24 @@ export function projectModelRenames(
226
346
  // Provider config is a closed interface, so index through one unknown-cast
227
347
  // view rather than casting at each assignment.
228
348
  const row = prov as unknown as Record<string, unknown>;
349
+ const seed = registrySeed(rename.provider);
229
350
  let touched = false;
230
351
  for (const field of MODEL_ID_LISTS) {
231
- const next = renameInList(row[field], rename.from, rename.to);
352
+ if (isRegistryResidue(seed, field, row[field], rename)) continue;
353
+ const next = rename.dropNoReasoningModels && field === "noReasoningModels"
354
+ ? dropRenamedIdsFromList(
355
+ row[field],
356
+ rename.from,
357
+ rename.to,
358
+ targetReasoningMatchesStaleSeed(row, rename),
359
+ )
360
+ : renameInList(row[field], rename.from, rename.to);
232
361
  if (!next) continue;
233
362
  row[field] = next;
234
363
  touched = true;
235
364
  }
236
365
  for (const field of MODEL_KEYED_RECORDS) {
366
+ if (isRegistryResidue(seed, field, row[field], rename)) continue;
237
367
  const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
238
368
  ? dropFromRecord(row[field], rename.from)
239
369
  : renameInRecord(row[field], rename.from, rename.to);
@@ -241,11 +371,22 @@ export function projectModelRenames(
241
371
  row[field] = next;
242
372
  touched = true;
243
373
  }
244
- if (prov.defaultModel === rename.from) {
245
- prov.defaultModel = rename.to;
374
+ for (const field of MODEL_NESTED_RECORDS) {
375
+ const container = row[field];
376
+ if (!container || typeof container !== "object" || Array.isArray(container)) continue;
377
+ const nested = container as Record<string, unknown>;
378
+ const next = renameInRecord(nested.models, rename.from, rename.to);
379
+ if (!next) continue;
380
+ row[field] = { ...nested, models: next };
381
+ touched = true;
382
+ }
383
+ for (const field of MODEL_ID_SCALARS) {
384
+ if (row[field] !== rename.from) continue;
385
+ row[field] = rename.to;
246
386
  touched = true;
247
387
  }
248
388
  if (renameDisabledModels(config, rename)) touched = true;
389
+ if (touched && refreshTargetSeed(row, rename)) touched = true;
249
390
 
250
391
  if (touched) {
251
392
  changed = true;
@@ -1,12 +1,13 @@
1
1
  import { mutatePersistedConfig } from "../config";
2
2
  import { projectModelRenames } from "./model-rename-migration";
3
3
  import { projectStaleContextWindows } from "./stale-context-window-migration";
4
+ import { projectStaleVisionClassifications } from "./stale-vision-classification-migration";
4
5
  import { projectDevinCliAuthMode } from "./devin-cli-authmode-migration";
5
6
  import type { OcxConfig } from "../types";
6
7
 
7
8
  /**
8
- * The startup projection: registry model renames, then the context-window
9
- * repair. Both fix a saved row the registry can no longer reach on its own —
9
+ * The startup projection: registry model renames, then the context-window and
10
+ * vision-classification repairs. All three fix a saved row the registry can no longer reach on its own —
10
11
  * `enrichProviderFromRegistry` backfills a missing field and never rewrites a
11
12
  * present one — so they share this pass rather than adding a second boot step
12
13
  * with its own persistence, adopt, and failure handling.
@@ -14,11 +15,12 @@ import type { OcxConfig } from "../types";
14
15
  export function projectStartupConfigRepairs(config: OcxConfig): ReturnType<typeof projectModelRenames> {
15
16
  const renames = projectModelRenames(config);
16
17
  const windows = projectStaleContextWindows(renames.config);
17
- const devinCli = projectDevinCliAuthMode(windows.config);
18
+ const vision = projectStaleVisionClassifications(windows.config);
19
+ const devinCli = projectDevinCliAuthMode(vision.config);
18
20
  return {
19
21
  config: devinCli.config,
20
- changed: renames.changed || windows.changed || devinCli.changed,
21
- warnings: [...renames.warnings, ...windows.warnings, ...devinCli.warnings],
22
+ changed: renames.changed || windows.changed || vision.changed || devinCli.changed,
23
+ warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...devinCli.warnings],
22
24
  };
23
25
  }
24
26
 
@@ -3,6 +3,9 @@ import { OPENAI_API_PROVIDER_ID } from "./openai-tiers";
3
3
  import type { OcxParsedRequest } from "../types";
4
4
  import type { RouteResult } from "../router";
5
5
  import type { RequestLogContext } from "../server/request-log";
6
+ import type { InboundWire } from "./registry/types";
7
+ import { providerMatchesRegistryTransportWithStaticGuards } from "./static-model-discovery";
8
+ import { resolveModelPolicy } from "./resolved-model-policy";
6
9
 
7
10
  export interface OpenAiVirtualModelResolution {
8
11
  selectedModelId: string;
@@ -49,18 +52,55 @@ export function resolveOpenAiVirtualModel(
49
52
  return validateOpenAiVirtualModelDefinition(selectedModelId, definition);
50
53
  }
51
54
 
55
+ export function captureOpenAiVirtualWirePolicy(
56
+ route: RouteResult,
57
+ resolution: OpenAiVirtualModelResolution,
58
+ inboundWire: InboundWire = "responses",
59
+ ): void {
60
+ // applyOpenAiVirtualModel is also a public mutation helper with older focused callers that
61
+ // construct only providerName/modelId/provider. Production RouteResult builders always capture
62
+ // staticPolicy; a legacy partial shape has no authority to recapture and keeps the old rewrite.
63
+ if (!route.staticPolicy) return;
64
+ const entry = PROVIDER_REGISTRY.find(candidate => candidate.id === route.providerName);
65
+ route.staticPolicy = resolveModelPolicy({
66
+ providerName: route.providerName,
67
+ modelId: resolution.wireModelId,
68
+ provider: route.provider,
69
+ registryEntry: entry,
70
+ transportMatchedRegistry: !!entry
71
+ && providerMatchesRegistryTransportWithStaticGuards(route.providerName, route.provider),
72
+ inboundWire,
73
+ modelCapabilities: route.provider.modelCapabilities?.[resolution.wireModelId],
74
+ ...(route.provider.authMode ? { effectiveAuth: { authMode: route.provider.authMode } } : {}),
75
+ ...(route.staticPolicy?.effectiveAlias !== undefined
76
+ ? { effectiveAlias: route.staticPolicy.effectiveAlias }
77
+ : {}),
78
+ });
79
+ }
80
+
52
81
  export function applyOpenAiVirtualModel(
53
82
  parsed: OcxParsedRequest,
54
83
  route: RouteResult,
55
84
  logCtx: RequestLogContext,
85
+ inboundWire: InboundWire = "responses",
56
86
  ): OpenAiVirtualModelResolution | undefined {
57
- const selectedModelId = logCtx.model && logCtx.model !== route.modelId ? logCtx.model : route.modelId;
58
- const resolution = resolveOpenAiVirtualModel(route.providerName, selectedModelId);
87
+ // Routing has already removed the provider namespace and resolved model aliases. Never use
88
+ // logCtx.model for identity: it is reporting state and can still contain a namespaced selector.
89
+ const resolution = resolveOpenAiVirtualModel(route.providerName, route.modelId)
90
+ ?? (() => {
91
+ // Preserve the public helper's idempotent second-call contract from its own mutation
92
+ // provenance, without promoting reporting state back into routing authority.
93
+ const selected = parsed._openAiVirtualSelectedModelId;
94
+ if (typeof selected !== "string") return undefined;
95
+ const remembered = resolveOpenAiVirtualModel(route.providerName, selected);
96
+ return remembered?.wireModelId === route.modelId ? remembered : undefined;
97
+ })();
59
98
  if (!resolution) return undefined;
60
99
 
61
100
  logCtx.model = resolution.selectedModelId;
62
101
  logCtx.resolvedModel = resolution.wireModelId;
63
102
  route.modelId = resolution.wireModelId;
103
+ captureOpenAiVirtualWirePolicy(route, resolution, inboundWire);
64
104
  parsed.modelId = resolution.wireModelId;
65
105
  parsed._openAiVirtualSelectedModelId = resolution.selectedModelId;
66
106
 
@@ -1,6 +1,6 @@
1
1
  import { antigravityUserAgent } from "../../adapters/client-fingerprint";
2
2
  import { DestinationDnsResolutionError } from "../../lib/destination-policy";
3
- import { PinnedHttpError } from "../../lib/pinned-http";
3
+ import { PinnedHttpError, type PinnedHttpErrorCode } from "../../lib/pinned-http";
4
4
  import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../../lib/provider-outbound";
5
5
  import { getValidAccessToken } from "../../oauth";
6
6
  import { getAccountCredential, getCredential } from "../../oauth/store";
@@ -193,10 +193,30 @@ type AntigravityQuotaProbeResult =
193
193
  | { kind: "available"; quota: ProviderQuota; source: "google-antigravity:retrieveUserQuotaSummary" | "google-antigravity:fetchAvailableModels" }
194
194
  | { kind: "unavailable"; failure: QuotaFailureCode; legacy: { kind: "null" } | { kind: "throw"; error: unknown } };
195
195
 
196
+ /**
197
+ * Every pinned-transport failure code, classified once.
198
+ *
199
+ * A conditional that named one code and sent the rest to `timeout` was correct only for as long
200
+ * as the union held exactly the codes it was written against. When the transport learned to
201
+ * report a coding it cannot undo, that answer was reported as a timeout, which is a different
202
+ * operational story entirely. A total map makes a new code a compile error here rather than a
203
+ * quiet misdiagnosis.
204
+ */
205
+ const PINNED_QUOTA_FAILURES = {
206
+ connect_timeout: "timeout",
207
+ first_byte_timeout: "timeout",
208
+ inactivity_timeout: "timeout",
209
+ // The response arrived and cannot be used: too large, coded in a format this transport cannot
210
+ // undo, or coded bytes that did not decode. None of these is a timing failure.
211
+ output_byte_limit: "response_unusable",
212
+ unsupported_content_encoding: "response_unusable",
213
+ content_decode_failed: "response_unusable",
214
+ } satisfies Record<PinnedHttpErrorCode, QuotaFailureCode>;
215
+
196
216
  function quotaTransportFailure(error: unknown): QuotaFailureCode {
197
217
  if (error instanceof ProviderOutboundPolicyError) return "destination_blocked";
198
218
  if (error instanceof DestinationDnsResolutionError) return "dns_failed";
199
- if (error instanceof PinnedHttpError) return error.code === "output_byte_limit" ? "response_unusable" : "timeout";
219
+ if (error instanceof PinnedHttpError) return PINNED_QUOTA_FAILURES[error.code];
200
220
  if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
201
221
  return "transport_error";
202
222
  }
@@ -1,6 +1,8 @@
1
1
  import { resolveProviderApiKey } from "../key-store";
2
2
  import { getProviderRegistryEntry, registryEntryForProviderDestination } from "../registry";
3
3
  import { isCanonicalOllamaCloudUrl } from "../../adapters/ollama-native-url";
4
+ import { providerEgressFetchInit, resolveProviderEgress } from "../../lib/provider-egress";
5
+ import { configuredOutboundFetch } from "../../lib/proxy-env";
4
6
  import { QUOTA_JSON_READ_FAILURE, asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
5
7
  import {
6
8
  AUTHORITATIVE_EMPTY_QUOTA,
@@ -15,6 +17,12 @@ import { getTokenForAccountQuotaProbe } from "./account-cache";
15
17
  import type { AccountQuotaMode, ProviderQuota, ProviderQuotaCreditsUsd } from "../quota-types";
16
18
  import type { OcxProviderConfig } from "../../types";
17
19
 
20
+ // A quota probe must use the provider's inference route so it neither reports a false healthy path nor leaks a key through another exit.
21
+ async function quotaFetch(providerName: string, config: OcxProviderConfig, url: string, init: RequestInit): Promise<Response> {
22
+ const egress = resolveProviderEgress({ providerName, provider: config, url });
23
+ return configuredOutboundFetch(url, { ...init, ...providerEgressFetchInit(egress) });
24
+ }
25
+
18
26
  const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
19
27
  const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
20
28
  const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
@@ -144,10 +152,10 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro
144
152
  if (!apiKey) return null;
145
153
  const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
146
154
  const [subscriptionResponse, tokenResponse] = await Promise.all([
147
- fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
155
+ quotaFetch(provider, config, `${A6API_BASE_URL}/dashboard/billing/subscription`, {
148
156
  headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
149
157
  }),
150
- fetch(`${A6API_BASE_URL}/api/usage/token/`, {
158
+ quotaFetch(provider, config, `${A6API_BASE_URL}/api/usage/token/`, {
151
159
  headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
152
160
  }),
153
161
  ]);
@@ -241,7 +249,7 @@ async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig)
241
249
  if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
242
250
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
243
251
  if (!apiKey) return null;
244
- const response = await fetch(OPENCODE_GO_USAGE_URL, {
252
+ const response = await quotaFetch(provider, config, OPENCODE_GO_USAGE_URL, {
245
253
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
246
254
  redirect: "error",
247
255
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -287,7 +295,7 @@ async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig)
287
295
  if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
288
296
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
289
297
  if (!apiKey) return null;
290
- const response = await fetch(`${OPENROUTER_BASE_URL}/key`, {
298
+ const response = await quotaFetch(provider, config, `${OPENROUTER_BASE_URL}/key`, {
291
299
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
292
300
  redirect: "error",
293
301
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -337,7 +345,7 @@ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig):
337
345
  if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
338
346
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
339
347
  if (!apiKey) return null;
340
- const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, {
348
+ const response = await quotaFetch(provider, config, `${DEEPSEEK_BASE_URL}/user/balance`, {
341
349
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
342
350
  redirect: "error",
343
351
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -382,7 +390,7 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
382
390
  if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
383
391
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
384
392
  if (!apiKey) return null;
385
- const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
393
+ const response = await quotaFetch(provider, config, `${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
386
394
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
387
395
  redirect: "error",
388
396
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -480,7 +488,7 @@ async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig
480
488
  if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
481
489
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
482
490
  if (!apiKey) return null;
483
- const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
491
+ const response = await quotaFetch(provider, config, OLLAMA_CLOUD_USAGE_URL, {
484
492
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
485
493
  redirect: "error",
486
494
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -604,7 +612,7 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
604
612
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
605
613
  if (!apiKey) return null;
606
614
  const authorization = monitorHost === ZAI_CN_BASE_URL ? apiKey : `Bearer ${apiKey}`;
607
- const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
615
+ const response = await quotaFetch(provider, config, `${monitorHost}/api/monitor/usage/quota/limit`, {
608
616
  headers: { Accept: "application/json", Authorization: authorization },
609
617
  redirect: "error",
610
618
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -652,7 +660,7 @@ async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): P
652
660
  if (!apiKey) return null;
653
661
  const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
654
662
  const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
655
- const response = await fetch(remainsUrl, {
663
+ const response = await quotaFetch(provider, config, remainsUrl, {
656
664
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
657
665
  redirect: "error",
658
666
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -695,7 +703,7 @@ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig):
695
703
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
696
704
  if (!apiKey) return null;
697
705
  const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
698
- const response = await fetch(`${host}/users/me/balance`, {
706
+ const response = await quotaFetch(provider, config, `${host}/users/me/balance`, {
699
707
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
700
708
  redirect: "error",
701
709
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -738,7 +746,7 @@ async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Pr
738
746
  if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
739
747
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
740
748
  if (!apiKey) return null;
741
- const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, {
749
+ const response = await quotaFetch(provider, config, `${VENICE_BASE_URL}/billing/balance`, {
742
750
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
743
751
  redirect: "error",
744
752
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -781,7 +789,7 @@ async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig):
781
789
  if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
782
790
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
783
791
  if (!apiKey) return null;
784
- const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, {
792
+ const response = await quotaFetch(provider, config, `${SYNTHETIC_BASE_URL}/quotas`, {
785
793
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
786
794
  redirect: "error",
787
795
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -831,7 +839,7 @@ async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig):
831
839
  if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
832
840
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
833
841
  if (!apiKey) return null;
834
- const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
842
+ const response = await quotaFetch(provider, config, `${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
835
843
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
836
844
  redirect: "error",
837
845
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -873,7 +881,7 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
873
881
  if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
874
882
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
875
883
  if (!apiKey) return null;
876
- const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, {
884
+ const response = await quotaFetch(provider, config, `${NEURALWATT_BASE_URL}/quota`, {
877
885
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
878
886
  redirect: "error",
879
887
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -915,7 +923,7 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
915
923
  }
916
924
 
917
925
 
918
- function normalizedBaseUrl(value: string): string | null {
926
+ export function normalizedBaseUrl(value: string): string | null {
919
927
  try {
920
928
  const url = new URL(value);
921
929
  if (url.username || url.password || url.search || url.hash) return null;
@@ -1050,7 +1058,7 @@ export async function fetchKimiQuota(provider: string, config: OcxProviderConfig
1050
1058
  // Never release credentials to a user-edited or lookalike provider host.
1051
1059
  if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
1052
1060
  if (!accessToken) return null;
1053
- const response = await fetch(KIMI_CODE_USAGE_URL, {
1061
+ const response = await quotaFetch(provider, config, KIMI_CODE_USAGE_URL, {
1054
1062
  headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
1055
1063
  redirect: "error",
1056
1064
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1077,9 +1085,14 @@ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: nu
1077
1085
  }
1078
1086
 
1079
1087
  /** Soft-fail GET returning a parsed record, or null when unavailable. */
1080
- async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
1088
+ async function fetchCommandCodeJson(
1089
+ provider: string,
1090
+ config: OcxProviderConfig,
1091
+ url: string,
1092
+ bearer: string,
1093
+ ): Promise<Record<string, unknown> | null> {
1081
1094
  try {
1082
- const response = await fetch(url, {
1095
+ const response = await quotaFetch(provider, config, url, {
1083
1096
  headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1084
1097
  redirect: "error",
1085
1098
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1097,12 +1110,14 @@ async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record
1097
1110
  * pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
1098
1111
  */
1099
1112
  async function fetchCommandCodeSpend(
1113
+ provider: string,
1114
+ config: OcxProviderConfig,
1100
1115
  bearer: string,
1101
1116
  credits: Record<string, unknown> | null,
1102
1117
  orgQuery: string,
1103
1118
  ): Promise<ProviderQuotaCreditsUsd | undefined> {
1104
1119
  if (!credits) return undefined;
1105
- const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
1120
+ const subscriptionBody = await fetchCommandCodeJson(provider, config, `${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
1106
1121
  const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
1107
1122
  const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
1108
1123
  // Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
@@ -1110,7 +1125,7 @@ async function fetchCommandCodeSpend(
1110
1125
  if (!periodStart) return undefined;
1111
1126
  const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
1112
1127
  const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
1113
- const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
1128
+ const summaryBody = await fetchCommandCodeJson(provider, config, `${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
1114
1129
  const summary = asRecord(summaryBody?.data) ?? summaryBody;
1115
1130
  const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
1116
1131
  if (used === undefined || used < 0) return undefined;
@@ -1161,12 +1176,12 @@ export async function fetchCommandCodeQuota(provider: string, config: OcxProvide
1161
1176
  // Never release credentials to a user-edited or lookalike provider host.
1162
1177
  if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
1163
1178
  if (!bearer) return null;
1164
- const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
1179
+ const whoamiBody = await fetchCommandCodeJson(provider, config, COMMAND_CODE_WHOAMI_URL, bearer);
1165
1180
  const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
1166
1181
  const org = asRecord(whoami?.org);
1167
1182
  const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
1168
1183
  const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
1169
- const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
1184
+ const response = await quotaFetch(provider, config, `${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
1170
1185
  headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1171
1186
  redirect: "error",
1172
1187
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1183,7 +1198,7 @@ export async function fetchCommandCodeQuota(provider: string, config: OcxProvide
1183
1198
  if (!credits && !limits) return null;
1184
1199
  const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
1185
1200
  const weekly = parseCommandCodeWindow(limits?.weekly);
1186
- const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
1201
+ const creditsUsd = await fetchCommandCodeSpend(provider, config, bearer, credits, orgQuery);
1187
1202
  const quota: ProviderQuota = {
1188
1203
  ...(fiveHour ? {
1189
1204
  fiveHourPercent: fiveHour.percent,