@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
@@ -11,7 +11,9 @@
11
11
  import { commitProviderApiKeySelection } from "./api-key-selection";
12
12
  import type { ProviderApiKeySelection } from "../types/provider";
13
13
  import { routedProviderConfig } from "../router";
14
- import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, TransientRetryPolicy } from "../types";
14
+ import { getProviderRegistryEntry } from "./registry";
15
+ import { normalizedBaseUrl } from "./quota/vendor-probes-key";
16
+ import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, ResetReplayPolicy, TransientRetryPolicy } from "../types";
15
17
  import { OPENCODE_GO_SESSION_HEADER } from "./opencode-go-transport";
16
18
  import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
17
19
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
@@ -278,6 +280,43 @@ const DEFAULT_RATE_LIMIT_RETRY = {
278
280
  respectRetryAfter: true,
279
281
  } as const satisfies Required<RateLimitRetryPolicy>;
280
282
 
283
+ /**
284
+ * Patient same-target 429 fallback for the OpenCode Go destination
285
+ * (https://opencode.ai/zen/go/v1), which serves subscription traffic such as Muse Spark.
286
+ * Single-key pools cannot fail over, so without this a burst 429 surfaces immediately and
287
+ * the client retry budget aborts the goal. Six 10s-paced attempts absorb a short burst
288
+ * window (effective replays are additionally bounded by the shared per-request send
289
+ * budget); Retry-After is still honored and capped. An explicit `retryOn429` (including
290
+ * `enabled: false`) always overrides this fallback.
291
+ */
292
+ const OPENCODE_GO_RATE_LIMIT_RETRY = {
293
+ enabled: true,
294
+ attempts: 6,
295
+ intervalMs: 10_000,
296
+ maxIntervalMs: 60_000,
297
+ respectRetryAfter: true,
298
+ } as const satisfies Required<RateLimitRetryPolicy>;
299
+
300
+ /** True when the provider row points at the OpenCode Go destination. */
301
+ export function isOpenCodeGoDestination(
302
+ provider: Partial<Pick<OcxProviderConfig, "baseUrl" | "authMode">>,
303
+ ): boolean {
304
+ const raw = typeof provider.baseUrl === "string" ? provider.baseUrl : "";
305
+ if (!raw.trim()) return false;
306
+ // Endpoint identity, not adapter identity: the runtime adapter is already overridden
307
+ // per model by the time the recovery loop runs (muse-spark rides `openai-responses`
308
+ // while the preset declares `openai-chat`), so an adapter-strict lookup misses it.
309
+ // Canonicalize with the shared quota-probe normalizer so host case and explicit
310
+ // default ports compare equal; userinfo, query, and fragment never match
311
+ // (follow-up to the review on #5067).
312
+ const endpoint = normalizedBaseUrl(raw.trim());
313
+ if (!endpoint) return false;
314
+ const entry = getProviderRegistryEntry("opencode-go");
315
+ if (!entry) return false;
316
+ const candidates = [entry.baseUrl, ...(entry.destinationAliases ?? []).map(alias => alias.baseUrl)];
317
+ return candidates.some(url => normalizedBaseUrl(url.trim()) === endpoint);
318
+ }
319
+
281
320
  /**
282
321
  * Default transient-5xx retry used when a provider opts in with a bare
283
322
  * `transientRetryOn5xx: {}`. `attempts` is a TOTAL send budget, not extra retries.
@@ -287,6 +326,16 @@ const DEFAULT_TRANSIENT_RETRY = {
287
326
  attempts: 3,
288
327
  } as const satisfies Required<TransientRetryPolicy>;
289
328
 
329
+ /**
330
+ * Default used when a provider opts in with a bare `retryOnReset: {}`: one replacement for
331
+ * the whole logical request. `replacements` counts duplicate inferences the operator accepts,
332
+ * not retries and not sends.
333
+ */
334
+ const DEFAULT_RESET_REPLAY = {
335
+ enabled: true,
336
+ replacements: 1,
337
+ } as const satisfies Required<ResetReplayPolicy>;
338
+
290
339
  /** Map<`${providerName}\0${keyId}`, KeyCooldown> */
291
340
  const keyCooldowns = new Map<string, KeyCooldown>();
292
341
 
@@ -513,29 +562,38 @@ export function selectProactiveApiKeyTransport(
513
562
  }
514
563
 
515
564
  /**
516
- * Normalize a provider's `retryOn429` policy, or return null when the knob is absent,
517
- * explicitly disabled, or the provider is not key-auth (OAuth/forward credentials must not be
518
- * replayed on the same token, forward passthrough never reaches the recovery loop anyway, and
519
- * local runtimes have no remote key to preserve). The returned policy is fully defaulted so
520
- * callers never re-check fields.
565
+ * Normalize a provider's `retryOn429` policy. An explicit object always wins (including
566
+ * `enabled: false` to opt out). When the knob is absent, the OpenCode Go destination
567
+ * (subscription traffic such as Muse Spark) falls back to a patient same-key policy so a
568
+ * burst 429 waits and replays instead of surfacing to the client and aborting a long
569
+ * session; every other provider without the knob keeps today's fail-fast behavior.
570
+ * OAuth/forward/local credentials are never replayed on the same token. The returned
571
+ * policy is fully defaulted so callers never re-check fields.
521
572
  */
522
573
  export function rateLimitRetryPolicyFor(
523
- provider: Pick<OcxProviderConfig, "retryOn429" | "authMode">,
574
+ provider: Pick<OcxProviderConfig, "retryOn429" | "authMode"> &
575
+ Partial<Pick<OcxProviderConfig, "baseUrl" | "adapter">>,
524
576
  ): Required<RateLimitRetryPolicy> | null {
525
577
  const policy = provider.retryOn429;
526
- if (!policy || policy.enabled === false) return null;
527
- // Fail closed: only explicit key auth or the documented omitted-default (undefined == key for
528
- // custom API-key providers) may use same-key replays. OAuth/forward are never replayed on the
529
- // same token, local runtimes have no remote key to preserve, and unknown/custom values are
530
- // rejected rather than guessed at.
578
+ if (policy) {
579
+ if (policy.enabled === false) return null;
580
+ // Fail closed: only explicit key auth or the documented omitted-default (undefined == key for
581
+ // custom API-key providers) may use same-key replays. OAuth/forward are never replayed on the
582
+ // same token, local runtimes have no remote key to preserve, and unknown/custom values are
583
+ // rejected rather than guessed at.
584
+ if (provider.authMode !== undefined && provider.authMode !== "key") return null;
585
+ return {
586
+ enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
587
+ attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
588
+ intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
589
+ maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
590
+ respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
591
+ };
592
+ }
593
+ // No explicit knob: patient fallback for the OpenCode Go destination only.
531
594
  if (provider.authMode !== undefined && provider.authMode !== "key") return null;
532
- return {
533
- enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
534
- attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
535
- intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
536
- maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
537
- respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
538
- };
595
+ if (!isOpenCodeGoDestination(provider)) return null;
596
+ return { ...OPENCODE_GO_RATE_LIMIT_RETRY };
539
597
  }
540
598
 
541
599
  /**
@@ -568,6 +626,26 @@ export function transientRetryPolicyFor(
568
626
  };
569
627
  }
570
628
 
629
+ /**
630
+ * Normalize a provider's `retryOnReset` policy, or return null when it is absent or explicitly
631
+ * disabled.
632
+ *
633
+ * No auth-mode gate: the canonical ChatGPT backend is `forward` auth and is the send this
634
+ * policy exists for. Whether a given REQUEST may be replaced is a per-body decision made in
635
+ * src/server/responses/reset-replay.ts, and how many replacements the request gets is the
636
+ * shared allowance on its execution budget. This function only reads the operator's intent.
637
+ */
638
+ export function resetReplayPolicyFor(
639
+ provider: Pick<OcxProviderConfig, "retryOnReset">,
640
+ ): Required<ResetReplayPolicy> | null {
641
+ const policy = provider.retryOnReset;
642
+ if (!policy || policy.enabled === false) return null;
643
+ return {
644
+ enabled: policy.enabled ?? DEFAULT_RESET_REPLAY.enabled,
645
+ replacements: policy.replacements ?? DEFAULT_RESET_REPLAY.replacements,
646
+ };
647
+ }
648
+
571
649
  /**
572
650
  * Wait before the next same-target replay: upstream Retry-After (seconds or HTTP-date) when
573
651
  * `respectRetryAfter` is on and the header parses, capped at `maxIntervalMs`; otherwise the
@@ -1,7 +1,34 @@
1
- import { createRequire } from "node:module";
2
1
  import { resolveEnvValue, saveConfigPreservingClaudeCode } from "../config";
3
2
  import type { OcxConfig, OcxProviderConfig } from "../types";
4
3
  import type { ProviderRegistryEntry } from "./registry";
4
+ import {
5
+ KEYCHAIN_REFERENCE_PREFIX,
6
+ invalidateResolvedProviderKeyCache,
7
+ isKeychainReference,
8
+ keychainAccount,
9
+ keychainReferenceBelongsToProvider,
10
+ probeProviderKeychain,
11
+ providerKeychainEntry,
12
+ } from "./api-key-resolve";
13
+
14
+ // The read-path (reference predicates, the OS entry factory, resolveProviderApiKey, the probe
15
+ // and the kind classification) lives in ./api-key-resolve -- a leaf module that
16
+ // reasoning-metadata can also import without pulling the ../config barrel into its
17
+ // cycle-sensitive graph. Re-exported here so existing key-store consumers keep working.
18
+ export {
19
+ KEYCHAIN_REFERENCE_PREFIX,
20
+ PROVIDER_KEYCHAIN_SERVICE,
21
+ isKeychainReference,
22
+ probeProviderKeychain,
23
+ providerKeyStoreKind,
24
+ resolveProviderApiKey,
25
+ setProviderKeychainEntryFactoryForTests,
26
+ } from "./api-key-resolve";
27
+ export type {
28
+ ProviderKeychainEntry,
29
+ ProviderKeychainEntryFactory,
30
+ ProviderKeyStoreKind,
31
+ } from "./api-key-resolve";
5
32
 
6
33
  /** Shared with routing: a key-mode override is effective only while its key resolves. */
7
34
  export function providerUsesKeyAuthOverride(
@@ -27,109 +54,8 @@ export function providerUsesKeyAuthOverride(
27
54
  * (headless service, locked session) refuses rather than half-migrating.
28
55
  */
29
56
 
30
- export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
31
- export const PROVIDER_KEYCHAIN_SERVICE = "opencodex.provider-api-key.v1";
32
-
33
- export interface ProviderKeychainEntry {
34
- getPassword(): string | null;
35
- setPassword(password: string): void;
36
- deletePassword(): boolean;
37
- }
38
-
39
- export type ProviderKeychainEntryFactory = (service: string, account: string) => ProviderKeychainEntry;
40
-
41
- const nodeRequire = createRequire(import.meta.url);
42
-
43
- function defaultEntryFactory(service: string, account: string): ProviderKeychainEntry {
44
- const { Entry } = nodeRequire("@napi-rs/keyring") as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
45
- return new Entry(service, account);
46
- }
47
-
48
- let entryFactory: ProviderKeychainEntryFactory = defaultEntryFactory;
49
- const resolvedCache = new Map<string, string>();
50
- const warnedAccounts = new Set<string>();
51
-
52
- /** Test seam: swap the OS entry for an in-memory one and drop caches. */
53
- export function setProviderKeychainEntryFactoryForTests(factory: ProviderKeychainEntryFactory | null): void {
54
- entryFactory = factory ?? defaultEntryFactory;
55
- resolvedCache.clear();
56
- warnedAccounts.clear();
57
- }
58
-
59
- export function isKeychainReference(value: string | undefined): value is string {
60
- return typeof value === "string" && value.startsWith(KEYCHAIN_REFERENCE_PREFIX) && value.length > KEYCHAIN_REFERENCE_PREFIX.length;
61
- }
62
-
63
- function keychainAccount(reference: string): string {
64
- return reference.slice(KEYCHAIN_REFERENCE_PREFIX.length);
65
- }
66
-
67
- /**
68
- * A reference belongs to `name` only when its account is that provider's own active account
69
- * or one of its pool accounts. `storeProviderKeyInKeychain` writes exactly those two shapes,
70
- * so anything else in a provider's config names another provider's secret.
71
- */
72
- function keychainReferenceBelongsToProvider(reference: string, name: string): boolean {
73
- const account = keychainAccount(reference);
74
- return account === name || account.startsWith(`${name}/`);
75
- }
76
-
77
- function readKeychain(account: string): string | undefined {
78
- const cached = resolvedCache.get(account);
79
- if (cached !== undefined) return cached;
80
- try {
81
- const value = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).getPassword();
82
- if (typeof value === "string" && value.trim()) {
83
- resolvedCache.set(account, value);
84
- return value;
85
- }
86
- } catch {
87
- // fall through to the single warning below
88
- }
89
- if (!warnedAccounts.has(account)) {
90
- warnedAccounts.add(account);
91
- console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
92
- }
93
- return undefined;
94
- }
95
-
96
- /**
97
- * Single resolver for provider key material: env references, keychain references, or the
98
- * literal value. Every request-time read of `apiKey` goes through here.
99
- */
100
- export function resolveProviderApiKey(value: string | undefined): string | undefined {
101
- if (!value) return undefined;
102
- if (isKeychainReference(value)) return readKeychain(keychainAccount(value));
103
- return resolveEnvValue(value);
104
- }
105
-
106
- export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
107
-
108
- export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
109
- const key = provider?.apiKey;
110
- if (!key) return "none";
111
- if (isKeychainReference(key)) return "keychain";
112
- if (/^\$\{?\w+\}?$/.test(key)) return "env";
113
- return "file";
114
- }
115
-
116
- /** Probe the OS keychain with a throwaway account: write, read back, delete. */
117
- export function probeProviderKeychain(): { available: true } | { available: false; reason: string } {
118
- const account = `probe-${process.pid}-${Date.now()}`;
119
- try {
120
- const entry = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
121
- entry.setPassword("ok");
122
- const back = entry.getPassword();
123
- try { entry.deletePassword(); } catch { /* best effort */ }
124
- if (back !== "ok") return { available: false, reason: "keychain read-back did not match" };
125
- return { available: true };
126
- } catch (error) {
127
- return { available: false, reason: error instanceof Error ? error.message : "keychain unavailable" };
128
- }
129
- }
130
-
131
57
  function writeVerified(account: string, secret: string): void {
132
- const entry = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
58
+ const entry = providerKeychainEntry(account);
133
59
  entry.setPassword(secret);
134
60
  if (entry.getPassword() !== secret) throw new Error(`keychain read-back mismatch for ${account}`);
135
61
  }
@@ -177,13 +103,12 @@ export function storeProviderKeyInKeychain(config: OcxConfig, name: string): { o
177
103
  }
178
104
  } catch (error) {
179
105
  for (const account of written) {
180
- try { entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).deletePassword(); } catch { /* best effort */ }
106
+ try { providerKeychainEntry(account).deletePassword(); } catch { /* best effort */ }
181
107
  }
182
108
  return { ok: false, error: `OS keychain write failed: ${error instanceof Error ? error.message : "unknown"}`, status: 503 };
183
109
  }
184
110
  for (const apply of planned) apply();
185
- resolvedCache.clear();
186
- warnedAccounts.clear();
111
+ invalidateResolvedProviderKeyCache();
187
112
  saveConfigPreservingClaudeCode(config);
188
113
  return { ok: true, moved: written.length };
189
114
  }
@@ -211,7 +136,7 @@ export function restoreProviderKeyFromKeychain(config: OcxConfig, name: string):
211
136
  const account = keychainAccount(ref);
212
137
  if (resolved.has(account)) continue;
213
138
  let value: string | null = null;
214
- try { value = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).getPassword(); } catch { value = null; }
139
+ try { value = providerKeychainEntry(account).getPassword(); } catch { value = null; }
215
140
  if (!value) return { ok: false, error: `OS keychain has no readable secret for ${ref}; config left unchanged`, status: 503 };
216
141
  resolved.set(account, value);
217
142
  }
@@ -220,10 +145,9 @@ export function restoreProviderKeyFromKeychain(config: OcxConfig, name: string):
220
145
  }
221
146
  if (isKeychainReference(provider.apiKey)) provider.apiKey = resolved.get(keychainAccount(provider.apiKey))!;
222
147
  for (const account of resolved.keys()) {
223
- try { entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).deletePassword(); } catch { /* best effort */ }
148
+ try { providerKeychainEntry(account).deletePassword(); } catch { /* best effort */ }
224
149
  }
225
- resolvedCache.clear();
226
- warnedAccounts.clear();
150
+ invalidateResolvedProviderKeyCache();
227
151
  saveConfigPreservingClaudeCode(config);
228
152
  return { ok: true, restored: resolved.size };
229
153
  }
@@ -0,0 +1,147 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+
3
+ // Every provider field receives a deliberate identity role. A new field fails typecheck
4
+ // until its model-id semantics are classified; credentials and provider-wide policy are none.
5
+ type ModelRenameRole = "none" | "record" | "list" | "scalar" | "nested-models";
6
+ export const PROVIDER_MODEL_RENAME_ROLES = {
7
+ alias: "none",
8
+ modelAliases: "record",
9
+ modelDisplayNames: "record",
10
+ defaultAliases: "none",
11
+ adapter: "none",
12
+ codexToolMode: "none",
13
+ requestPacing: "nested-models",
14
+ mcpMaxTools: "none",
15
+ mcpMaxSchemaBytes: "none",
16
+ mcpMaxResultBytes: "none",
17
+ modelAdapters: "record",
18
+ fastWire: "none",
19
+ baseUrl: "none",
20
+ responsesPath: "none",
21
+ chatCompletionsPath: "none",
22
+ commandCodeVersion: "none",
23
+ statelessResponses: "none",
24
+ requiresAdjacentResponsesToolResults: "none",
25
+ requiresPairedResponsesToolResults: "none",
26
+ annotateEmptyToolOutputs: "none",
27
+ supportsServiceTier: "none",
28
+ modelSupportsServiceTier: "record",
29
+ preserveResponsesReasoningContent: "none",
30
+ modelReasoningEffortsAuthoritative: "none",
31
+ dropResponsesReasoningItems: "none",
32
+ decodesNativeCompactionBlobs: "none",
33
+ allowEncryptedV2AgentTasks: "none",
34
+ allowPrivateNetwork: "none",
35
+ proxy: "none",
36
+ noProxy: "none",
37
+ upstreamHttpVersion: "none",
38
+ upstreamWebsocket: "none",
39
+ directGeminiWireRenames: "none",
40
+ disabled: "none",
41
+ codexAccountMode: "none",
42
+ apiKey: "none",
43
+ apiKeyTransport: "none",
44
+ apiKeyPool: "none",
45
+ apiKeyPoolStrategy: "none",
46
+ apiKeySelectionRevision: "none",
47
+ _apiKeyAttempt: "none",
48
+ defaultModel: "scalar",
49
+ models: "list",
50
+ liveModels: "none",
51
+ selectedModels: "list",
52
+ initialModelSelection: "none",
53
+ retainModels: "list",
54
+ newModelPolicy: "none",
55
+ modelPreset: "none",
56
+ contextWindow: "none",
57
+ modelContextWindows: "record",
58
+ modelInputModalities: "record",
59
+ modelCapabilities: "record",
60
+ modelMaxInputTokens: "record",
61
+ modelAutoCompactTokenLimits: "record",
62
+ defaultMaxOutputTokens: "none",
63
+ modelMaxOutputTokens: "record",
64
+ modelCosts: "record",
65
+ autoReviewModel: "none",
66
+ autoReviewModelOverrides: "record",
67
+ headers: "none",
68
+ openRouterRouting: "none",
69
+ modelOpenRouterRouting: "record",
70
+ vercelGatewayRouting: "none",
71
+ modelVercelGatewayRouting: "record",
72
+ authMode: "none",
73
+ oauthAccountFailover: "none",
74
+ keyOptional: "none",
75
+ freeTier: "none",
76
+ note: "none",
77
+ modelSuffixBracketStrip: "none",
78
+ refreshPolicy: "none",
79
+ reasoningEfforts: "none",
80
+ modelReasoningEfforts: "record",
81
+ modelSuppressSyntheticMax: "record",
82
+ modelDefaultReasoningEfforts: "record",
83
+ pinnedReasoningEffort: "none",
84
+ modelPinnedReasoningEfforts: "record",
85
+ modelSupportsReasoningSummaries: "record",
86
+ modelSupportsVerbosity: "record",
87
+ supportsVerbosity: "none",
88
+ modelReasoningSummaryDelivery: "record",
89
+ modelPreferHostedTools: "record",
90
+ supportsOpenAiWebSearchToolFields: "none",
91
+ xaiResponsesXSearch: "none",
92
+ xaiResponsesDefaultVersion: "none",
93
+ zaiResponsesDefaultVersion: "none",
94
+ supportsResponsesCustomTools: "none",
95
+ unsupportedHostedTools: "none",
96
+ responsesSnapshotRepair: "none",
97
+ webSearchBridge: "none",
98
+ reasoningEffortMap: "none",
99
+ modelReasoningEffortMap: "record",
100
+ reasoningWireFormat: "none",
101
+ noReasoningModels: "list",
102
+ noTemperatureModels: "list",
103
+ noTopPModels: "list",
104
+ noPenaltyModels: "list",
105
+ noStructuredOutputModels: "list",
106
+ noJsonSchemaModels: "list",
107
+ omitReasoningEffortWithToolsModels: "list",
108
+ parallelToolCalls: "none",
109
+ pinParallelToolCallsFalse: "none",
110
+ terminalContinuationGuard: "none",
111
+ openaiChatEofTolerance: "none",
112
+ foldDeveloperRoleToSystem: "none",
113
+ promptCacheKey: "none",
114
+ chatServiceTier: "none",
115
+ responsesItemIdRepair: "none",
116
+ autoToolChoiceOnlyModels: "list",
117
+ preserveReasoningContentModels: "list",
118
+ requiresReasoningPlaceholderModels: "list",
119
+ showThinkingSummary: "none",
120
+ retryOn429: "none",
121
+ transientRetryOn5xx: "none",
122
+ retryOnReset: "none",
123
+ reasoningSplitModels: "list",
124
+ reasoningDetailsModels: "list",
125
+ thinkingToggleModels: "list",
126
+ thinkingBudgetModels: "list",
127
+ escapeBuiltinToolNames: "none",
128
+ anthropicEofTolerance: "none",
129
+ noVisionModels: "list",
130
+ googleMode: "none",
131
+ googleToolSchemaPolicy: "none",
132
+ project: "none",
133
+ location: "none",
134
+ mcpServers: "none",
135
+ desktopExecutor: "none",
136
+ unsafeAllowNativeLocalExec: "none",
137
+ nativeLocalExec: "none",
138
+ } as const satisfies Record<keyof OcxProviderConfig, ModelRenameRole>;
139
+
140
+ function fieldsWithRole(role: ModelRenameRole): string[] {
141
+ return Object.entries(PROVIDER_MODEL_RENAME_ROLES).filter(([, value]) => value === role).map(([key]) => key);
142
+ }
143
+ export const MODEL_KEYED_RECORDS = fieldsWithRole("record");
144
+ // Legacy persisted registry field, no longer part of the public provider config interface.
145
+ export const MODEL_ID_LISTS = [...fieldsWithRole("list"), "directReasoningEffortModels"];
146
+ export const MODEL_ID_SCALARS = fieldsWithRole("scalar");
147
+ export const MODEL_NESTED_RECORDS = fieldsWithRole("nested-models");