@iislee/opencodex 2.11.0 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Which models may serve AS the vision sidecar (the describer), as opposed to the
3
+ * models the sidecar describes FOR.
4
+ *
5
+ * Three rules make this non-obvious, and all are load-bearing:
6
+ *
7
+ * 1. `provider.noVisionModels` marks models the proxy describes images for, and
8
+ * `applyProviderConfigHints` deliberately ADDS "image" to their advertised
9
+ * input modalities so the Codex app does not block the attachment client-side.
10
+ * A blind model therefore advertises image input. Membership in that list is a
11
+ * hard disqualifier here, checked BEFORE the modality list it rewrote.
12
+ * 2. The catalog enriches configured providers from the registry before it
13
+ * applies that rule. Eligibility must use the same effective provider policy,
14
+ * including a `noVisionModels` list learned after the config was persisted.
15
+ * 3. Catalog rows frequently omit `inputModalities` entirely (the live
16
+ * `/api/models` response carries none for either openai or anthropic rows).
17
+ * Unknown is not zero: when no source can speak, the model stays eligible
18
+ * rather than silently vanishing from the picker.
19
+ * Native OpenAI metadata is likewise authoritative only for a native row or
20
+ * the canonical OpenAI provider; a routed row with the same id owns its own
21
+ * explicit modalities.
22
+ */
23
+ import { modelInList, type OcxConfig, type OcxProviderConfig } from "../types";
24
+ import { getModelMetadataCaseInsensitive, resolveMetadataProvider } from "../generated/model-metadata";
25
+ import { nativeInputModalities } from "../codex/catalog/metadata";
26
+ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
27
+ import { enrichProviderFromRegistry } from "../providers/derive";
28
+
29
+ /**
30
+ * The wire protocols `planVisionSidecar` can dispatch to (#2188 roadmap 170
31
+ * REVISED). "routed" describes through the proxy's OWN router via loopback —
32
+ * one executor for every non-forward, non-OAuth-Anthropic provider row.
33
+ */
34
+ export type VisionSidecarBackend = "openai" | "anthropic" | "routed";
35
+
36
+ /** The two sides every deployment has; also the empty-auth fallback set. */
37
+ export type UniversalVisionBackend = "openai" | "anthropic";
38
+
39
+ /**
40
+ * Default entry per backend: cheap, image-capable, and present in every deployment. Offered
41
+ * whenever its side is enabled, and withheld only when that provider explicitly lists it as a
42
+ * model the sidecar describes FOR — never merely because a metadata table stayed silent.
43
+ *
44
+ * Keyed by the UNIVERSAL subset on purpose (roadmap 170, audit blocker A):
45
+ * xai/gemini are auth-gated sides whose catalogs are present whenever the side
46
+ * is, so they carry no baseline, and a narrow-key total record documents that
47
+ * without sprinkling non-null assertions at the consumers.
48
+ */
49
+ export const BASELINE_VISION_MODELS: Record<UniversalVisionBackend, string> = {
50
+ openai: "gpt-5.6-luna",
51
+ anthropic: "claude-haiku-4-5",
52
+ };
53
+
54
+ export interface VisionCandidateModel {
55
+ provider: string;
56
+ id: string;
57
+ inputModalities?: string[];
58
+ native?: boolean;
59
+ }
60
+
61
+ export interface VisionModelOption {
62
+ value: string;
63
+ label: string;
64
+ backend: VisionSidecarBackend;
65
+ /** True when the row is a guaranteed baseline rather than a catalog discovery. */
66
+ baseline?: boolean;
67
+ }
68
+
69
+ type EnrichedProviderCache = Map<string, OcxProviderConfig>;
70
+
71
+ function advertisesImageInput(modalities: readonly string[] | undefined): boolean | undefined {
72
+ if (!modalities || modalities.length === 0) return undefined;
73
+ return modalities.includes("image");
74
+ }
75
+
76
+ /** Vendor-table modalities for a routed row, or undefined when the table has no opinion. */
77
+ function metadataImageInput(provider: string, modelId: string): boolean | undefined {
78
+ const resolved = resolveMetadataProvider(provider) ?? provider;
79
+ const meta = getModelMetadataCaseInsensitive(resolved, modelId);
80
+ return advertisesImageInput(meta?.input);
81
+ }
82
+
83
+ /**
84
+ * Mirrors the catalog's registry enrichment without mutating saved configuration. The picker
85
+ * checks many rows from one provider, so its caller shares this small call-local cache.
86
+ */
87
+ function enrichedProviderForVision(
88
+ config: Pick<OcxConfig, "providers">,
89
+ providerName: string,
90
+ cache: EnrichedProviderCache,
91
+ ): OcxProviderConfig | undefined {
92
+ const cached = cache.get(providerName);
93
+ if (cached) return cached;
94
+ const configured = config.providers?.[providerName];
95
+ if (!configured) return undefined;
96
+ const enriched = structuredClone(configured);
97
+ enrichProviderFromRegistry(providerName, enriched);
98
+ cache.set(providerName, enriched);
99
+ return enriched;
100
+ }
101
+
102
+ function isVisionSidecarConsumerWithCache(
103
+ config: Pick<OcxConfig, "providers">,
104
+ providerName: string,
105
+ modelId: string,
106
+ cache: EnrichedProviderCache,
107
+ ): boolean {
108
+ return modelInList(enrichedProviderForVision(config, providerName, cache)?.noVisionModels, modelId);
109
+ }
110
+
111
+ /**
112
+ * Is this model listed as one the sidecar describes FOR? Such a model cannot be
113
+ * the describer, and its advertised modalities are untrustworthy.
114
+ */
115
+ export function isVisionSidecarConsumer(config: Pick<OcxConfig, "providers">, providerName: string, modelId: string): boolean {
116
+ return isVisionSidecarConsumerWithCache(config, providerName, modelId, new Map());
117
+ }
118
+
119
+ /**
120
+ * Can this model accept an image on the wire? Sources are consulted in descending
121
+ * trustworthiness; the first that speaks wins. `undefined` means nothing knows,
122
+ * which callers treat as eligible.
123
+ */
124
+ export function modelAcceptsImageInput(
125
+ config: Pick<OcxConfig, "providers">,
126
+ candidate: VisionCandidateModel,
127
+ ): boolean | undefined {
128
+ return modelAcceptsImageInputWithCache(config, candidate, new Map());
129
+ }
130
+
131
+ function modelAcceptsImageInputWithCache(
132
+ config: Pick<OcxConfig, "providers">,
133
+ candidate: VisionCandidateModel,
134
+ cache: EnrichedProviderCache,
135
+ ): boolean | undefined {
136
+ if (isVisionSidecarConsumerWithCache(config, candidate.provider, candidate.id, cache)) return false;
137
+ if (candidate.native === true || (candidate.provider === "openai" && SUPPORTED_NATIVE_OPENAI_SLUGS.has(candidate.id))) {
138
+ return advertisesImageInput(nativeInputModalities(candidate.id)) ?? true;
139
+ }
140
+ const fromRow = advertisesImageInput(candidate.inputModalities);
141
+ if (fromRow !== undefined) return fromRow;
142
+ return metadataImageInput(candidate.provider, candidate.id);
143
+ }
144
+
145
+ /** Eligible = not a sidecar consumer, and not positively known to be text-only. */
146
+ export function isVisionEligibleModel(
147
+ config: Pick<OcxConfig, "providers">,
148
+ candidate: VisionCandidateModel,
149
+ ): boolean {
150
+ return isVisionEligibleModelWithCache(config, candidate, new Map());
151
+ }
152
+
153
+ function isVisionEligibleModelWithCache(
154
+ config: Pick<OcxConfig, "providers">,
155
+ candidate: VisionCandidateModel,
156
+ cache: EnrichedProviderCache,
157
+ ): boolean {
158
+ return modelAcceptsImageInputWithCache(config, candidate, cache) !== false;
159
+ }
160
+
161
+ /** Which executor can describe through this row. */
162
+ export function visionBackendForCandidate(
163
+ config: Pick<OcxConfig, "providers">,
164
+ candidate: VisionCandidateModel,
165
+ anthropicProviderName?: string,
166
+ ): VisionSidecarBackend | undefined {
167
+ if (candidate.native || candidate.provider === "openai") return "openai";
168
+ // The runtime dispatches through exactly ONE Anthropic provider — the enabled OAuth row
169
+ // with a healthy active account that `findAnthropicVisionProvider` picks. Speaking the
170
+ // Messages wire is not enough: a key-auth row of the same adapter is unreachable, and an
171
+ // option that cannot be dispatched is worse than a missing one, because selecting it fails
172
+ // at describe time rather than at pick time.
173
+ if (anthropicProviderName !== undefined && candidate.provider === anthropicProviderName) {
174
+ return "anthropic";
175
+ }
176
+ // EVERY other provider row describes through the proxy's own router
177
+ // (roadmap 170 revised): the loopback executor covers all provider wires,
178
+ // so no row is left without an executor.
179
+ return "routed";
180
+ }
181
+
182
+ function baselineCandidate(
183
+ backend: UniversalVisionBackend,
184
+ anthropicProviderName: string | undefined,
185
+ ): VisionCandidateModel {
186
+ return {
187
+ provider: backend === "openai" ? "openai" : anthropicProviderName ?? "anthropic",
188
+ id: BASELINE_VISION_MODELS[backend],
189
+ // Both baselines are known image-capable. This makes their only exclusion path the
190
+ // provider's explicit consumer list, never a silent or stale metadata table.
191
+ inputModalities: ["text", "image"],
192
+ };
193
+ }
194
+
195
+ /**
196
+ * The picker's option list: every eligible row reachable by an enabled
197
+ * executor, plus each enabled universal side's baseline unless that baseline
198
+ * is explicitly excluded, de-duplicated and stably ordered (side rank order,
199
+ * baselines first within a side). Anthropic rows must belong to the OAuth
200
+ * provider that would actually execute them, so `anthropicProviderName` is
201
+ * what makes that side's catalog rows eligible at all; xai/gemini rows map by
202
+ * provider identity and appear only when the caller enabled those backends.
203
+ *
204
+ * This is the SUGGESTION list (narrow): it emits only rows an executor can reach
205
+ * and some source has heard of. It is deliberately NOT the same set as the write
206
+ * gate. `modelAcceptsImageInput` answers "can we prove this model cannot see?"
207
+ * and is the only input to rejection. Absence from this list must never imply
208
+ * rejection — an unknown id stays eligible via the undefined → eligible fallback.
209
+ *
210
+ * De-duplication is by BARE model id, first eligible row wins. Two providers of
211
+ * the same adapter family can expose the same id; they resolve to the same
212
+ * backend, and only `value` reaches the client, so first-wins costs nothing.
213
+ */
214
+ export function visionEligibleModelOptions(
215
+ config: Pick<OcxConfig, "providers">,
216
+ candidates: readonly VisionCandidateModel[],
217
+ enabledBackends: readonly VisionSidecarBackend[],
218
+ anthropicProviderName?: string,
219
+ ): VisionModelOption[] {
220
+ const enabled = new Set(enabledBackends);
221
+ const byValue = new Map<string, VisionModelOption>();
222
+ const enrichedProviders: EnrichedProviderCache = new Map();
223
+
224
+ for (const backend of Object.keys(BASELINE_VISION_MODELS) as UniversalVisionBackend[]) {
225
+ if (!enabled.has(backend)) continue;
226
+ const candidate = baselineCandidate(backend, anthropicProviderName);
227
+ if (!isVisionEligibleModelWithCache(config, candidate, enrichedProviders)) continue;
228
+ const id = candidate.id;
229
+ byValue.set(id, { value: id, label: id, backend, baseline: true });
230
+ }
231
+ for (const candidate of candidates) {
232
+ const backend = visionBackendForCandidate(config, candidate, anthropicProviderName);
233
+ if (!backend || !enabled.has(backend)) continue;
234
+ if (!isVisionEligibleModelWithCache(config, candidate, enrichedProviders)) continue;
235
+ // Routed rows carry NAMESPACED values ("provider/model") so the loopback
236
+ // dispatch is unambiguous under routeModel; the legacy sides keep bare ids
237
+ // (GUI current-value compatibility, and the forward/OAuth executors POST
238
+ // the string verbatim). De-dup stays keyed by the emitted value.
239
+ const value = backend === "routed" ? `${candidate.provider}/${candidate.id}` : candidate.id;
240
+ if (byValue.has(value)) continue;
241
+ byValue.set(value, { value, label: value, backend });
242
+ }
243
+
244
+ // Two slots per side (baseline first), ranked openai < anthropic < routed
245
+ // so widening the union appends rather than interleaves (roadmap 170).
246
+ const sideRank: Record<VisionSidecarBackend, number> = { openai: 0, anthropic: 2, routed: 4 };
247
+ const order = (option: VisionModelOption) =>
248
+ sideRank[option.backend] + (option.baseline ? 0 : 1);
249
+ return [...byValue.values()].sort((a, b) => order(a) - order(b) || a.value.localeCompare(b.value));
250
+ }
@@ -1,22 +1,62 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { OcxConfig, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent } from "../types";
3
3
  import { modelInList } from "../types";
4
+ import { modelRecordValue } from "../reasoning-effort";
5
+ import type { VisionReasoningEffort } from "../reasoning-effort";
4
6
  import { describeImage, type DescribeOutcome, type VisionSettings } from "./describe";
5
7
  import { describeImageAnthropic } from "./anthropic-describe";
8
+ import { describeImageRouted } from "./routed-describe";
9
+ import { modelAcceptsImageInput } from "./eligibility";
10
+ import { normalizeVisionReasoningForModel } from "./reasoning";
6
11
  import type { CodexAuthContext } from "../codex/auth-context";
7
- import { getAccountSet } from "../oauth/store";
12
+ import { resolveSidecarAuth } from "../sidecar/auth";
8
13
  import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
9
14
  import type { SidecarOutcomeRecorder } from "../web-search/executor";
10
15
  import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
11
16
  import type { TranslatorBudget } from "../lib/translator-budget";
17
+ import {
18
+ DEFAULT_VISION_TIMEOUT_MS,
19
+ MAX_VISION_TIMEOUT_MS,
20
+ MIN_VISION_TIMEOUT_MS,
21
+ } from "./timeout-bounds";
12
22
 
13
23
  export { describeImage } from "./describe";
24
+
25
+ /**
26
+ * True when the model is explicitly known to be text-only — either listed in
27
+ * `noVisionModels` or declared with `modelInputModalities` that exclude "image".
28
+ * Returns false for unknown models (no evidence either way) so they fall through
29
+ * to native image passthrough, which is the safe default for an unclassified model.
30
+ */
31
+ export function isModelTextOnly(
32
+ provider: OcxProviderConfig,
33
+ modelId: string,
34
+ ): boolean {
35
+ if (modelInList(provider.noVisionModels, modelId)) return true;
36
+ const modalities = modelRecordValue(provider.modelInputModalities, modelId);
37
+ if (Array.isArray(modalities) && modalities.length > 0 && !modalities.includes("image")) return true;
38
+ return false;
39
+ }
14
40
  export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
41
+ export {
42
+ BASELINE_VISION_MODELS,
43
+ isVisionEligibleModel,
44
+ isVisionSidecarConsumer,
45
+ modelAcceptsImageInput,
46
+ visionBackendForCandidate,
47
+ visionEligibleModelOptions,
48
+ } from "./eligibility";
49
+ export type { VisionCandidateModel, VisionModelOption, VisionSidecarBackend } from "./eligibility";
50
+ export {
51
+ DEFAULT_VISION_TIMEOUT_MS,
52
+ MAX_VISION_TIMEOUT_MS,
53
+ MIN_VISION_TIMEOUT_MS,
54
+ };
15
55
 
16
56
  const DEFAULT_VISION_MODEL = "gpt-5.4-mini";
17
57
  const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5";
18
- const DEFAULT_TIMEOUT_MS = 45_000;
19
- const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8;
58
+ const DEFAULT_REASONING: VisionReasoningEffort = "low";
59
+ export const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8;
20
60
  const DESCRIPTION_CACHE_MAX_ENTRIES = 256;
21
61
  export const VISION_DESCRIPTION_CACHE_MAX_BYTES = 1024 * 1024;
22
62
  const descriptionEncoder = new TextEncoder();
@@ -142,6 +182,18 @@ export function resolveMaxDescriptionsPerTurn(value: unknown): number {
142
182
  : DEFAULT_MAX_DESCRIPTIONS_PER_TURN;
143
183
  }
144
184
 
185
+ export function isValidVisionTimeoutMs(value: unknown): value is number {
186
+ return typeof value === "number"
187
+ && Number.isInteger(value)
188
+ && value >= MIN_VISION_TIMEOUT_MS
189
+ && value <= MAX_VISION_TIMEOUT_MS;
190
+ }
191
+
192
+ /** Runtime config is permissive: malformed or out-of-range values fall back to the default. */
193
+ export function resolveVisionTimeoutMs(value: unknown): number {
194
+ return isValidVisionTimeoutMs(value) ? value : DEFAULT_VISION_TIMEOUT_MS;
195
+ }
196
+
145
197
  /** Run `worker` over `items` with bounded concurrency, preserving input order in the result array. */
146
198
  async function runBounded<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>): Promise<R[]> {
147
199
  const results = new Array<R>(items.length);
@@ -165,25 +217,52 @@ export interface AnthropicVisionProvider {
165
217
  provider: OcxProviderConfig;
166
218
  }
167
219
 
168
- /** First enabled Anthropic OAuth provider whose active stored account is not marked for reauth. */
220
+ /**
221
+ * First enabled Anthropic OAuth provider whose active stored account is not marked for reauth.
222
+ * Delegates to the shared sidecar auth module (#2188) — same predicate as web-search.
223
+ */
169
224
  export function findAnthropicVisionProvider(config: OcxConfig): AnthropicVisionProvider | undefined {
170
- for (const [providerName, provider] of Object.entries(config.providers)) {
171
- if (provider.disabled === true || provider.adapter !== "anthropic" || provider.authMode !== "oauth") continue;
172
- const accountSet = getAccountSet(providerName);
173
- const active = accountSet?.accounts.find(account => account.id === accountSet.activeAccountId);
174
- if (active && active.needsReauth !== true) return { providerName, provider };
175
- }
176
- return undefined;
225
+ const auth = resolveSidecarAuth(config);
226
+ if (!auth.isAnthropicAuth || !auth.anthropicProviderName || !auth.anthropicProvider) return undefined;
227
+ return { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider };
177
228
  }
178
229
 
179
230
  export function resolveVisionBackend(
180
- explicit: "openai" | "anthropic" | undefined,
231
+ explicit: "openai" | "anthropic" | "routed" | undefined,
181
232
  anthropicSidecar: AnthropicVisionProvider | undefined,
182
233
  ): "openai" | "anthropic" {
183
234
  if (explicit === "openai" || explicit === "anthropic") return explicit;
235
+ // "routed" collapses to the legacy default order until its describe executor
236
+ // lands (roadmap 170 → 180 revised): a persisted routed backend without a
237
+ // dispatchable arm degrades exactly like unset rather than crashing. wp3
238
+ // replaces this collapse with the real routed arm in planVisionSidecar.
184
239
  return anthropicSidecar ? "anthropic" : "openai";
185
240
  }
186
241
 
242
+ /** Native model used by the OpenAI vision helper, including its bounded default. */
243
+ export function resolveOpenAiVisionModel(config: Pick<OcxConfig, "visionSidecar">): string {
244
+ const configured = config.visionSidecar?.model;
245
+ // Namespaced routed ids never reach the forward executor (see
246
+ // resolveEffectiveVisionModel).
247
+ return configured && !configured.includes("/") ? configured : DEFAULT_VISION_MODEL;
248
+ }
249
+
250
+ /** Effective describer model for the backend `planVisionSidecar` selected. */
251
+ export function resolveEffectiveVisionModel(
252
+ config: Pick<OcxConfig, "visionSidecar">,
253
+ backend: "openai" | "anthropic",
254
+ ): string {
255
+ const configured = config.visionSidecar?.model;
256
+ // A namespaced "provider/model" id belongs to the routed backend only; the
257
+ // forward/OAuth executors POST the model string verbatim, so it falls back
258
+ // to the side's default here (PUT coherence rejects new writes of this
259
+ // shape, but a legacy or hand-edited config must not break the executor).
260
+ const usable = configured && !configured.includes("/") ? configured : undefined;
261
+ return backend === "anthropic"
262
+ ? usable || DEFAULT_ANTHROPIC_VISION_MODEL
263
+ : usable || DEFAULT_VISION_MODEL;
264
+ }
265
+
187
266
  /** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */
188
267
  function carriesImages(role: string): boolean {
189
268
  return role === "user" || role === "developer" || role === "toolResult";
@@ -197,19 +276,23 @@ function messagesHaveImage(parsed: OcxParsedRequest): boolean {
197
276
  export function shouldResolveOpenAiVisionSidecar(
198
277
  config: OcxConfig,
199
278
  provider: OcxProviderConfig,
200
- modelId: string,
201
- parsed: OcxParsedRequest,
279
+ modelId: string,
280
+ parsed: OcxParsedRequest,
202
281
  ): boolean {
203
- if (!modelInList(provider.noVisionModels, modelId) || !messagesHaveImage(parsed)) return false;
282
+ if (!isModelTextOnly(provider, modelId) || !messagesHaveImage(parsed)) return false;
204
283
  const cfg = config.visionSidecar ?? {};
205
284
  if (cfg.enabled === false) return false;
206
285
  return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai";
207
286
  }
208
287
 
209
288
  export interface VisionPlan {
210
- backend: "openai" | "anthropic";
289
+ backend: "openai" | "anthropic" | "routed";
211
290
  forwardSidecar?: ResolvedOpenAiForwardSidecar;
212
291
  anthropicSidecar?: AnthropicVisionProvider;
292
+ /** Namespaced "provider/model" describer for the routed backend (roadmap 180). */
293
+ routedModel?: string;
294
+ /** Loopback dispatch inputs for the routed backend. */
295
+ routedConfig?: Pick<OcxConfig, "port" | "apiKeys">;
213
296
  settings: VisionSettings;
214
297
  maxDescriptionsPerTurn: number;
215
298
  }
@@ -227,12 +310,50 @@ export function planVisionSidecar(
227
310
  parsed: OcxParsedRequest,
228
311
  openAiSidecar?: ResolvedOpenAiForwardSidecar,
229
312
  ): VisionPlan | undefined {
230
- if (!modelInList(provider.noVisionModels, modelId)) return undefined;
313
+ if (!isModelTextOnly(provider, modelId)) return undefined;
231
314
  if (!messagesHaveImage(parsed)) return undefined;
232
315
  const cfg = config.visionSidecar ?? {};
233
316
  if (cfg.enabled === false) return undefined;
317
+
318
+ // Routed arm (roadmap 180 revised): explicit backend + NAMESPACED explicit
319
+ // model only — never inferred from credential availability. Plan-time
320
+ // fence: the target must not be provably blind, and must not itself be a
321
+ // model this planner would re-enter for (belt; the terminal marker on the
322
+ // loopback request is the braces).
323
+ if (cfg.backend === "routed") {
324
+ const routedModel = cfg.model;
325
+ const sep = routedModel ? routedModel.indexOf("/") : -1;
326
+ if (routedModel && sep > 0) {
327
+ const targetProvider = routedModel.slice(0, sep);
328
+ const targetId = routedModel.slice(sep + 1);
329
+ const targetProviderConfig = config.providers?.[targetProvider];
330
+ const targetVisible = modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false
331
+ && !(targetProviderConfig && isModelTextOnly(targetProviderConfig, targetId));
332
+ if (targetVisible) {
333
+ return {
334
+ backend: "routed",
335
+ routedModel,
336
+ routedConfig: { port: config.port, ...(config.apiKeys ? { apiKeys: config.apiKeys } : {}) },
337
+ settings: {
338
+ model: routedModel,
339
+ reasoning: DEFAULT_REASONING,
340
+ timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
341
+ },
342
+ maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn),
343
+ };
344
+ }
345
+ }
346
+ // Misconfigured routed backend (bare id, unknown provider, or provably
347
+ // blind target): fall through to the legacy default order below rather
348
+ // than dispatching a describe that cannot work.
349
+ }
350
+
234
351
  const anthropicSidecar = findAnthropicVisionProvider(config);
235
352
  const backend = resolveVisionBackend(cfg.backend, anthropicSidecar);
353
+ // A namespaced routed model must never reach the forward/OAuth executors
354
+ // (they POST the string verbatim); the effective-model resolver falls back
355
+ // to each side's default in that case.
356
+ const model = resolveEffectiveVisionModel(config, backend);
236
357
  const maxDescriptionsPerTurn = resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn);
237
358
 
238
359
  if (backend === "anthropic") {
@@ -240,7 +361,11 @@ export function planVisionSidecar(
240
361
  return {
241
362
  backend,
242
363
  anthropicSidecar,
243
- settings: { model: cfg.model ?? DEFAULT_ANTHROPIC_VISION_MODEL, timeoutMs: cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS },
364
+ settings: {
365
+ model,
366
+ reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING,
367
+ timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
368
+ },
244
369
  maxDescriptionsPerTurn,
245
370
  };
246
371
  }
@@ -249,7 +374,11 @@ export function planVisionSidecar(
249
374
  return {
250
375
  backend,
251
376
  forwardSidecar: openAiSidecar,
252
- settings: { model: cfg.model ?? DEFAULT_VISION_MODEL, timeoutMs: cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS },
377
+ settings: {
378
+ model,
379
+ reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING,
380
+ timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
381
+ },
253
382
  maxDescriptionsPerTurn,
254
383
  };
255
384
  }
@@ -270,6 +399,72 @@ function renderDescription(out: { text: string; error?: string }): OcxTextConten
270
399
  };
271
400
  }
272
401
 
402
+ const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]";
403
+
404
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
405
+ return typeof value === "object" && value !== null && !Array.isArray(value);
406
+ }
407
+
408
+ /**
409
+ * Keep the native Responses passthrough body aligned with image replacements made in the parsed
410
+ * message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize
411
+ * `context.messages`; updating only the latter would send the original pixels to a text-only
412
+ * Responses upstream even after the vision sidecar produced a caption.
413
+ *
414
+ * Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses
415
+ * items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally
416
+ * untouched.
417
+ */
418
+ function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void {
419
+ const rawBody = parsed._rawBody;
420
+ if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return;
421
+
422
+ let nextDescription = 0;
423
+ const rewriteImages = (value: unknown, nonEmptyImageUrlsOnly: boolean): unknown => {
424
+ if (Array.isArray(value)) {
425
+ let changed = false;
426
+ const rewritten = value.map(entry => {
427
+ const next = rewriteImages(entry, nonEmptyImageUrlsOnly);
428
+ if (next !== entry) changed = true;
429
+ return next;
430
+ });
431
+ return changed ? rewritten : value;
432
+ }
433
+ if (!isPlainRecord(value)) return value;
434
+ if (value.type === "input_image" && typeof value.image_url === "string") {
435
+ if (nonEmptyImageUrlsOnly && value.image_url.length === 0) {
436
+ return { type: "input_text", text: IMAGE_OMITTED_TEXT };
437
+ }
438
+ const description = descriptions[nextDescription++];
439
+ return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT };
440
+ }
441
+ return value;
442
+ };
443
+
444
+ let changed = false;
445
+ const input = rawBody.input.map(item => {
446
+ if (!isPlainRecord(item)) return item;
447
+ const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : "");
448
+ const role = typeof item.role === "string" ? item.role : "";
449
+ const isMessageContent = (
450
+ (type === "message" && (role === "user" || role === "developer"))
451
+ || type === "agent_message"
452
+ );
453
+ const field = isMessageContent
454
+ ? "content"
455
+ : (type === "function_call_output" || type === "custom_tool_call_output")
456
+ ? "output"
457
+ : undefined;
458
+ if (!field) return item;
459
+ const rewritten = rewriteImages(item[field], isMessageContent);
460
+ if (rewritten === item[field]) return item;
461
+ changed = true;
462
+ return { ...item, [field]: rewritten };
463
+ });
464
+
465
+ if (changed) rawBody.input = input;
466
+ }
467
+
273
468
  function sha256(value: string | Uint8Array): string {
274
469
  return createHash("sha256").update(value).digest("hex");
275
470
  }
@@ -292,6 +487,7 @@ function descriptionIdentity(job: ImageJob, plan: VisionPlan): { key: string; pe
292
487
  key: JSON.stringify([
293
488
  plan.backend,
294
489
  plan.settings.model,
490
+ ...(plan.backend === "openai" ? [plan.settings.reasoning] : []),
295
491
  job.detail ?? "high",
296
492
  imageHash,
297
493
  sha256(normalizedContext(job.contextText)),
@@ -307,6 +503,18 @@ async function executeDescription(
307
503
  abortSignal?: AbortSignal,
308
504
  recordSidecarOutcome?: SidecarOutcomeRecorder,
309
505
  ): Promise<DescribeOutcome> {
506
+ if (plan.backend === "routed") {
507
+ if (!plan.routedModel || !plan.routedConfig) return { text: "", error: "routed vision sidecar is unavailable" };
508
+ return describeImageRouted(
509
+ job.imageUrl,
510
+ job.detail,
511
+ job.contextText,
512
+ plan.routedModel,
513
+ plan.routedConfig,
514
+ plan.settings,
515
+ abortSignal,
516
+ );
517
+ }
310
518
  if (plan.backend === "anthropic") {
311
519
  const sidecar = plan.anthropicSidecar;
312
520
  if (!sidecar) return { text: "", error: "anthropic vision sidecar is unavailable" };
@@ -347,7 +555,6 @@ export async function describeImagesInPlace(
347
555
  recordSidecarOutcome?: SidecarOutcomeRecorder,
348
556
  translatorBudget?: TranslatorBudget,
349
557
  ): Promise<void> {
350
- // 1. Gather every image part across messages, each with its own message's text as context.
351
558
  const jobs: ImageJob[] = [];
352
559
  const targets: { msg: OcxMessage; parts: OcxContentPart[] }[] = [];
353
560
  for (const msg of parsed.context.messages) {
@@ -364,9 +571,11 @@ export async function describeImagesInPlace(
364
571
  }
365
572
  targets.push({ msg, parts });
366
573
  }
367
- if (jobs.length === 0) return;
574
+ if (jobs.length === 0) {
575
+ syncRawBodyImageDescriptions(parsed, []);
576
+ return;
577
+ }
368
578
 
369
- // 2. Admit misses in source order. Cache hits and same-turn waiters do not consume the cap.
370
579
  const inFlight = new Map<string, Promise<DescribeOutcome>>();
371
580
  const executions: Array<() => Promise<void>> = [];
372
581
  const outcomePromises: Array<Promise<DescribeOutcome>> = [];
@@ -418,8 +627,8 @@ export async function describeImagesInPlace(
418
627
  await runBounded(executions, VISION_CONCURRENCY, execute => execute());
419
628
  const outcomes = await Promise.all(outcomePromises);
420
629
 
421
- // 3. Rebuild each message, replacing image parts with their descriptions in order.
422
630
  let oi = 0;
631
+ const descriptions: string[] = [];
423
632
  for (const { msg, parts } of targets) {
424
633
  const newParts: OcxContentPart[] = [];
425
634
  for (const p of parts) {
@@ -428,6 +637,7 @@ export async function describeImagesInPlace(
428
637
  continue;
429
638
  }
430
639
  const replacement = renderDescription(outcomes[oi++]);
640
+ descriptions.push(replacement.text);
431
641
  const reservation = translatorBudget?.reserveTransient(
432
642
  descriptionEncoder.encode(replacement.text).byteLength,
433
643
  { kind: "request_copies" },
@@ -437,6 +647,7 @@ export async function describeImagesInPlace(
437
647
  }
438
648
  msg.content = newParts;
439
649
  }
650
+ syncRawBodyImageDescriptions(parsed, descriptions);
440
651
  }
441
652
 
442
653
  /**
@@ -447,13 +658,15 @@ export async function describeImagesInPlace(
447
658
  */
448
659
  export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
449
660
  let stripped = false;
661
+ const descriptions: string[] = [];
450
662
  for (const msg of parsed.context.messages) {
451
663
  if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
452
664
  const parts = msg.content as OcxContentPart[];
453
665
  if (!parts.some(p => p.type === "image")) continue;
454
666
  msg.content = parts.map(p => {
455
667
  if (p.type !== "image") return p;
456
- const replacement = { type: "text", text: "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]" } as OcxContentPart;
668
+ const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart;
669
+ descriptions.push((replacement as OcxTextContent).text);
457
670
  const reservation = translatorBudget?.reserveTransient(
458
671
  descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
459
672
  { kind: "request_copies" },
@@ -463,5 +676,6 @@ export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?:
463
676
  });
464
677
  stripped = true;
465
678
  }
679
+ syncRawBodyImageDescriptions(parsed, descriptions);
466
680
  return stripped;
467
681
  }