@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,467 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { OcxConfig, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent } from "../types";
3
+ import { modelInList } from "../types";
4
+ import { describeImage, type DescribeOutcome, type VisionSettings } from "./describe";
5
+ import { describeImageAnthropic } from "./anthropic-describe";
6
+ import type { CodexAuthContext } from "../codex/auth-context";
7
+ import { getAccountSet } from "../oauth/store";
8
+ import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
9
+ import type { SidecarOutcomeRecorder } from "../web-search/executor";
10
+ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
11
+ import type { TranslatorBudget } from "../lib/translator-budget";
12
+
13
+ export { describeImage } from "./describe";
14
+ export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
15
+
16
+ const DEFAULT_VISION_MODEL = "gpt-5.4-mini";
17
+ const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5";
18
+ const DEFAULT_TIMEOUT_MS = 45_000;
19
+ const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8;
20
+ const DESCRIPTION_CACHE_MAX_ENTRIES = 256;
21
+ export const VISION_DESCRIPTION_CACHE_MAX_BYTES = 1024 * 1024;
22
+ const descriptionEncoder = new TextEncoder();
23
+ /** Max images described in parallel — keeps first-token latency bounded without flooding the backend. */
24
+ const VISION_CONCURRENCY = 3;
25
+ /** Per-image description hard cap (chars) so multi-image turns can't blow the main model's context. */
26
+ const DESC_MAX_CHARS = 2000;
27
+ /** User-text context passed to the describer, capped. */
28
+ const CONTEXT_MAX_CHARS = 800;
29
+
30
+ export interface VisionDescriptionCache {
31
+ get(key: string): string | undefined;
32
+ set(key: string, value: string): void;
33
+ clear(): void;
34
+ snapshot?(): { count: number; bytes: number; oldestAt: number | null };
35
+ evictOldest?(): number;
36
+ }
37
+
38
+ class BoundedLruDescriptionCache implements VisionDescriptionCache {
39
+ private readonly entries = new Map<string, { value: string; sizeBytes: number; storedAt: number }>();
40
+ private bytes = 0;
41
+
42
+ constructor(private readonly maxEntries: number, private readonly maxBytes: number) {}
43
+
44
+ get(key: string): string | undefined {
45
+ const entry = this.entries.get(key);
46
+ if (entry === undefined) return undefined;
47
+ this.entries.delete(key);
48
+ entry.storedAt = Date.now();
49
+ this.entries.set(key, entry);
50
+ return entry.value;
51
+ }
52
+
53
+ set(key: string, value: string): void {
54
+ const existing = this.entries.get(key);
55
+ if (existing) {
56
+ this.entries.delete(key);
57
+ this.bytes -= existing.sizeBytes;
58
+ }
59
+ const sizeBytes = descriptionEncoder.encode(key).byteLength + descriptionEncoder.encode(value).byteLength;
60
+ if (sizeBytes > this.maxBytes || this.maxEntries <= 0) return;
61
+ while (this.entries.size + 1 > this.maxEntries || this.bytes + sizeBytes > this.maxBytes) {
62
+ if (this.evictOldest() === 0) return;
63
+ }
64
+ this.entries.set(key, { value, sizeBytes, storedAt: Date.now() });
65
+ this.bytes += sizeBytes;
66
+ }
67
+
68
+ clear(): void {
69
+ this.entries.clear();
70
+ this.bytes = 0;
71
+ }
72
+
73
+ snapshot(): { count: number; bytes: number; oldestAt: number | null } {
74
+ return {
75
+ count: this.entries.size,
76
+ bytes: this.bytes,
77
+ oldestAt: this.entries.values().next().value?.storedAt ?? null,
78
+ };
79
+ }
80
+
81
+ evictOldest(): number {
82
+ const oldest = this.entries.keys().next().value;
83
+ if (oldest === undefined) return 0;
84
+ const entry = this.entries.get(oldest);
85
+ if (!entry) return 0;
86
+ this.entries.delete(oldest);
87
+ this.bytes -= entry.sizeBytes;
88
+ return entry.sizeBytes;
89
+ }
90
+ }
91
+
92
+ let descriptionCacheLimits = {
93
+ maxEntries: DESCRIPTION_CACHE_MAX_ENTRIES,
94
+ maxBytes: VISION_DESCRIPTION_CACHE_MAX_BYTES,
95
+ };
96
+
97
+ function defaultDescriptionCache(): VisionDescriptionCache {
98
+ return new BoundedLruDescriptionCache(descriptionCacheLimits.maxEntries, descriptionCacheLimits.maxBytes);
99
+ }
100
+
101
+ let descriptionCache: VisionDescriptionCache = defaultDescriptionCache();
102
+
103
+ /** Replace the process cache (primarily for deterministic tests). Passing undefined restores the default LRU. */
104
+ export function setVisionDescriptionCache(cache?: VisionDescriptionCache): void {
105
+ descriptionCache = cache ?? defaultDescriptionCache();
106
+ }
107
+
108
+ export function resetVisionDescriptionCache(): void {
109
+ descriptionCache.clear();
110
+ }
111
+
112
+ export function setVisionDescriptionCacheLimitsForTests(
113
+ limits?: { maxEntries?: number; maxBytes?: number },
114
+ ): void {
115
+ descriptionCacheLimits = limits
116
+ ? { maxEntries: limits.maxEntries ?? DESCRIPTION_CACHE_MAX_ENTRIES, maxBytes: limits.maxBytes ?? VISION_DESCRIPTION_CACHE_MAX_BYTES }
117
+ : { maxEntries: DESCRIPTION_CACHE_MAX_ENTRIES, maxBytes: VISION_DESCRIPTION_CACHE_MAX_BYTES };
118
+ descriptionCache = defaultDescriptionCache();
119
+ }
120
+
121
+ export function visionDescriptionRetainedStoreSnapshot(): {
122
+ count: number;
123
+ bytes: number;
124
+ evictableBytes: number;
125
+ pinnedBytes: number;
126
+ oldestAt: number | null;
127
+ } {
128
+ const snapshot = descriptionCache.snapshot?.();
129
+ if (!snapshot) return { count: 0, bytes: 0, evictableBytes: 0, pinnedBytes: 0, oldestAt: null };
130
+ return { ...snapshot, evictableBytes: snapshot.bytes, pinnedBytes: 0 };
131
+ }
132
+
133
+ export function evictOldestVisionDescriptionForBudget(): number {
134
+ return descriptionCache.evictOldest?.() ?? 0;
135
+ }
136
+
137
+ /** Runtime config is permissive: zero is intentional; malformed values fall back to the bounded default. */
138
+ export function resolveMaxDescriptionsPerTurn(value: unknown): number {
139
+ if (value === 0) return 0;
140
+ return typeof value === "number" && Number.isInteger(value) && value > 0
141
+ ? value
142
+ : DEFAULT_MAX_DESCRIPTIONS_PER_TURN;
143
+ }
144
+
145
+ /** Run `worker` over `items` with bounded concurrency, preserving input order in the result array. */
146
+ async function runBounded<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>): Promise<R[]> {
147
+ const results = new Array<R>(items.length);
148
+ let next = 0;
149
+ const runner = async (): Promise<void> => {
150
+ while (next < items.length) {
151
+ const i = next++;
152
+ results[i] = await worker(items[i]);
153
+ }
154
+ };
155
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
156
+ return results;
157
+ }
158
+
159
+ function clamp(s: string, max: number): string {
160
+ return s.length <= max ? s : `${s.slice(0, max)}\n…[description truncated]`;
161
+ }
162
+
163
+ export interface AnthropicVisionProvider {
164
+ providerName: string;
165
+ provider: OcxProviderConfig;
166
+ }
167
+
168
+ /** First enabled Anthropic OAuth provider whose active stored account is not marked for reauth. */
169
+ 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;
177
+ }
178
+
179
+ export function resolveVisionBackend(
180
+ explicit: "openai" | "anthropic" | undefined,
181
+ anthropicSidecar: AnthropicVisionProvider | undefined,
182
+ ): "openai" | "anthropic" {
183
+ if (explicit === "openai" || explicit === "anthropic") return explicit;
184
+ return anthropicSidecar ? "anthropic" : "openai";
185
+ }
186
+
187
+ /** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */
188
+ function carriesImages(role: string): boolean {
189
+ return role === "user" || role === "developer" || role === "toolResult";
190
+ }
191
+
192
+ function messagesHaveImage(parsed: OcxParsedRequest): boolean {
193
+ return parsed.context.messages.some(m =>
194
+ carriesImages(m.role) && Array.isArray(m.content) && (m.content as OcxContentPart[]).some(p => p.type === "image"));
195
+ }
196
+
197
+ export function shouldResolveOpenAiVisionSidecar(
198
+ config: OcxConfig,
199
+ provider: OcxProviderConfig,
200
+ modelId: string,
201
+ parsed: OcxParsedRequest,
202
+ ): boolean {
203
+ if (!modelInList(provider.noVisionModels, modelId) || !messagesHaveImage(parsed)) return false;
204
+ const cfg = config.visionSidecar ?? {};
205
+ if (cfg.enabled === false) return false;
206
+ return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai";
207
+ }
208
+
209
+ export interface VisionPlan {
210
+ backend: "openai" | "anthropic";
211
+ forwardSidecar?: ResolvedOpenAiForwardSidecar;
212
+ anthropicSidecar?: AnthropicVisionProvider;
213
+ settings: VisionSettings;
214
+ maxDescriptionsPerTurn: number;
215
+ }
216
+
217
+ /**
218
+ * Decide whether the vision sidecar should pre-describe images for this request, returning the plan
219
+ * if so. Active when: the routed model is in `provider.noVisionModels`, the request actually carries
220
+ * an image, the sidecar isn't disabled, and the selected backend has usable auth. Returns undefined
221
+ * otherwise (the caller strips images before sending to a text-only model).
222
+ */
223
+ export function planVisionSidecar(
224
+ config: OcxConfig,
225
+ provider: OcxProviderConfig,
226
+ modelId: string,
227
+ parsed: OcxParsedRequest,
228
+ openAiSidecar?: ResolvedOpenAiForwardSidecar,
229
+ ): VisionPlan | undefined {
230
+ if (!modelInList(provider.noVisionModels, modelId)) return undefined;
231
+ if (!messagesHaveImage(parsed)) return undefined;
232
+ const cfg = config.visionSidecar ?? {};
233
+ if (cfg.enabled === false) return undefined;
234
+ const anthropicSidecar = findAnthropicVisionProvider(config);
235
+ const backend = resolveVisionBackend(cfg.backend, anthropicSidecar);
236
+ const maxDescriptionsPerTurn = resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn);
237
+
238
+ if (backend === "anthropic") {
239
+ if (!anthropicSidecar) return undefined;
240
+ return {
241
+ backend,
242
+ anthropicSidecar,
243
+ settings: { model: cfg.model ?? DEFAULT_ANTHROPIC_VISION_MODEL, timeoutMs: cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS },
244
+ maxDescriptionsPerTurn,
245
+ };
246
+ }
247
+
248
+ if (!openAiSidecar) return undefined;
249
+ return {
250
+ backend,
251
+ forwardSidecar: openAiSidecar,
252
+ settings: { model: cfg.model ?? DEFAULT_VISION_MODEL, timeoutMs: cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS },
253
+ maxDescriptionsPerTurn,
254
+ };
255
+ }
256
+
257
+ interface ImageJob {
258
+ imageUrl: string;
259
+ detail?: string;
260
+ contextText: string;
261
+ }
262
+
263
+ /** Render one describe outcome as the replacement text part (clamped to the per-image budget). */
264
+ function renderDescription(out: { text: string; error?: string }): OcxTextContent {
265
+ return {
266
+ type: "text",
267
+ text: out.error
268
+ ? `[An image was attached but could not be processed: ${out.error}]`
269
+ : `[Image content — described by a vision model because you cannot see images directly:\n${clamp(out.text.trim(), DESC_MAX_CHARS)}]`,
270
+ };
271
+ }
272
+
273
+ function sha256(value: string | Uint8Array): string {
274
+ return createHash("sha256").update(value).digest("hex");
275
+ }
276
+
277
+ function normalizedContext(contextText: string): string {
278
+ return contextText.trim().replace(/\s+/g, " ");
279
+ }
280
+
281
+ function descriptionIdentity(job: ImageJob, plan: VisionPlan): { key: string; persistent: boolean } {
282
+ let imageHash: string;
283
+ let persistent = false;
284
+ const data = /^data:[^;,]+;base64,(.*)$/s.exec(job.imageUrl);
285
+ if (data) {
286
+ imageHash = sha256(Buffer.from(data[1], "base64"));
287
+ persistent = true;
288
+ } else {
289
+ imageHash = sha256(job.imageUrl);
290
+ }
291
+ return {
292
+ key: JSON.stringify([
293
+ plan.backend,
294
+ plan.settings.model,
295
+ job.detail ?? "high",
296
+ imageHash,
297
+ sha256(normalizedContext(job.contextText)),
298
+ ]),
299
+ persistent,
300
+ };
301
+ }
302
+
303
+ async function executeDescription(
304
+ job: ImageJob,
305
+ plan: VisionPlan,
306
+ selectedForwardHeaders: Headers,
307
+ abortSignal?: AbortSignal,
308
+ recordSidecarOutcome?: SidecarOutcomeRecorder,
309
+ ): Promise<DescribeOutcome> {
310
+ if (plan.backend === "anthropic") {
311
+ const sidecar = plan.anthropicSidecar;
312
+ if (!sidecar) return { text: "", error: "anthropic vision sidecar is unavailable" };
313
+ return describeImageAnthropic(
314
+ job.imageUrl,
315
+ job.detail,
316
+ job.contextText,
317
+ sidecar.providerName,
318
+ sidecar.provider,
319
+ plan.settings,
320
+ abortSignal,
321
+ );
322
+ }
323
+ if (!plan.forwardSidecar) return { text: "", error: "OpenAI vision sidecar is unavailable" };
324
+ return describeImage(
325
+ job.imageUrl,
326
+ job.detail,
327
+ job.contextText,
328
+ plan.forwardSidecar.provider,
329
+ plan.forwardSidecar.headers,
330
+ plan.settings,
331
+ abortSignal,
332
+ recordSidecarOutcome,
333
+ );
334
+ }
335
+
336
+ /**
337
+ * Replace every image part in the request with a gpt-described text part, so a text-only model can
338
+ * reason about it. Mutates `parsed.context.messages` in place; uses the message's own text as the
339
+ * description context. All images are described with bounded concurrency (not serially) so a
340
+ * multi-image turn doesn't pay the sum of per-image latencies. Failures degrade to a short marker.
341
+ */
342
+ export async function describeImagesInPlace(
343
+ parsed: OcxParsedRequest,
344
+ plan: VisionPlan,
345
+ selectedForwardHeaders: Headers,
346
+ abortSignal?: AbortSignal,
347
+ recordSidecarOutcome?: SidecarOutcomeRecorder,
348
+ translatorBudget?: TranslatorBudget,
349
+ ): Promise<void> {
350
+ // 1. Gather every image part across messages, each with its own message's text as context.
351
+ const jobs: ImageJob[] = [];
352
+ const targets: { msg: OcxMessage; parts: OcxContentPart[] }[] = [];
353
+ for (const msg of parsed.context.messages) {
354
+ if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
355
+ const parts = msg.content as OcxContentPart[];
356
+ if (!parts.some(p => p.type === "image")) continue;
357
+ const contextText = parts
358
+ .filter((p): p is OcxTextContent => p.type === "text")
359
+ .map(p => p.text)
360
+ .join(" ")
361
+ .slice(0, CONTEXT_MAX_CHARS);
362
+ for (const p of parts) {
363
+ if (p.type === "image") jobs.push({ imageUrl: p.imageUrl, detail: p.detail, contextText });
364
+ }
365
+ targets.push({ msg, parts });
366
+ }
367
+ if (jobs.length === 0) return;
368
+
369
+ // 2. Admit misses in source order. Cache hits and same-turn waiters do not consume the cap.
370
+ const inFlight = new Map<string, Promise<DescribeOutcome>>();
371
+ const executions: Array<() => Promise<void>> = [];
372
+ const outcomePromises: Array<Promise<DescribeOutcome>> = [];
373
+ let misses = 0;
374
+
375
+ for (const job of jobs) {
376
+ const identity = descriptionIdentity(job, plan);
377
+ const cached = identity.persistent ? descriptionCache.get(identity.key) : undefined;
378
+ if (cached !== undefined) {
379
+ outcomePromises.push(Promise.resolve({ text: cached }));
380
+ continue;
381
+ }
382
+
383
+ const existing = inFlight.get(identity.key);
384
+ if (existing) {
385
+ outcomePromises.push(existing);
386
+ continue;
387
+ }
388
+
389
+ if (misses >= plan.maxDescriptionsPerTurn) {
390
+ const capped = Promise.resolve<DescribeOutcome>({ text: "", error: "description cap reached for this turn" });
391
+ inFlight.set(identity.key, capped);
392
+ outcomePromises.push(capped);
393
+ continue;
394
+ }
395
+
396
+ misses += 1;
397
+ let resolveOutcome!: (outcome: DescribeOutcome) => void;
398
+ const pending = new Promise<DescribeOutcome>(resolve => { resolveOutcome = resolve; });
399
+ inFlight.set(identity.key, pending);
400
+ outcomePromises.push(pending);
401
+ executions.push(async () => {
402
+ let outcome: DescribeOutcome;
403
+ try {
404
+ outcome = await executeDescription(job, plan, selectedForwardHeaders, abortSignal, recordSidecarOutcome);
405
+ } catch (error) {
406
+ outcome = { text: "", error: error instanceof Error ? error.message : String(error) };
407
+ }
408
+ const successfulText = outcome.error ? "" : clamp(outcome.text.trim(), DESC_MAX_CHARS);
409
+ if (identity.persistent && successfulText) {
410
+ descriptionCache.set(identity.key, successfulText);
411
+ enforceAppOwnedMemoryBudget();
412
+ }
413
+ const resolvedOutcome = outcome.error ? outcome : { ...outcome, text: successfulText };
414
+ resolveOutcome(resolvedOutcome);
415
+ });
416
+ }
417
+
418
+ await runBounded(executions, VISION_CONCURRENCY, execute => execute());
419
+ const outcomes = await Promise.all(outcomePromises);
420
+
421
+ // 3. Rebuild each message, replacing image parts with their descriptions in order.
422
+ let oi = 0;
423
+ for (const { msg, parts } of targets) {
424
+ const newParts: OcxContentPart[] = [];
425
+ for (const p of parts) {
426
+ if (p.type !== "image") {
427
+ newParts.push(p);
428
+ continue;
429
+ }
430
+ const replacement = renderDescription(outcomes[oi++]);
431
+ const reservation = translatorBudget?.reserveTransient(
432
+ descriptionEncoder.encode(replacement.text).byteLength,
433
+ { kind: "request_copies" },
434
+ );
435
+ newParts.push(replacement);
436
+ reservation?.commitRetained();
437
+ }
438
+ msg.content = newParts;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward
444
+ * provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding
445
+ * raw images would 400 or silently confuse it. Replace each image with an explicit marker so the
446
+ * model (and the user, via its reply) knows the image was dropped rather than ignored.
447
+ */
448
+ export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
449
+ let stripped = false;
450
+ for (const msg of parsed.context.messages) {
451
+ if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
452
+ const parts = msg.content as OcxContentPart[];
453
+ if (!parts.some(p => p.type === "image")) continue;
454
+ msg.content = parts.map(p => {
455
+ 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;
457
+ const reservation = translatorBudget?.reserveTransient(
458
+ descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
459
+ { kind: "request_copies" },
460
+ );
461
+ reservation?.commitRetained();
462
+ return replacement;
463
+ });
464
+ stripped = true;
465
+ }
466
+ return stripped;
467
+ }
@@ -0,0 +1,189 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import { getValidAccessToken } from "../oauth";
3
+ import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION } from "../oauth/anthropic";
4
+ import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "../adapters/client-fingerprint";
5
+ import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
6
+ import { redactSecretString } from "../lib/redact";
7
+ import { sidecarEnter } from "../lib/sidecar-tracker";
8
+ import { fetchWithResetRetry } from "../lib/upstream-retry";
9
+ import type { WebSearchSource } from "./parse";
10
+ import { BASE_INSTRUCTION, IMAGE_INSTRUCTION, type SidecarOutcome, type SidecarSettings } from "./executor";
11
+
12
+ /** Hardcoded per-turn search bound handed to the server tool (mirrors the loop's maxSearches intent). */
13
+ const ANTHROPIC_MAX_USES = 3;
14
+ /** Answer budget; the injected tool_result is clamped downstream, so this only bounds the sidecar turn. */
15
+ const ANTHROPIC_MAX_TOKENS = 8192;
16
+
17
+ function isRec(v: unknown): v is Record<string, unknown> {
18
+ return !!v && typeof v === "object" && !Array.isArray(v);
19
+ }
20
+
21
+ /**
22
+ * Fold an Anthropic Messages SSE stream (a web_search_20250305 turn) into a WebSearchResult.
23
+ *
24
+ * Anthropic streams the FULL `web_search_tool_result.content` array on `content_block_start` (not via
25
+ * deltas), so sources are collected there; the answer text arrives as `text_delta` events, and
26
+ * `citations_delta` (web_search_result_location) contributes any additional cited URLs. A
27
+ * `web_search_tool_result_error` content object yields no sources. Never throws.
28
+ */
29
+ export async function parseAnthropicSidecarSSE(res: Response): Promise<SidecarOutcome> {
30
+ const sources: WebSearchSource[] = [];
31
+ const seen = new Set<string>();
32
+ const pushSource = (url: unknown, title: unknown): void => {
33
+ if (typeof url !== "string" || url.length === 0 || seen.has(url)) return;
34
+ seen.add(url);
35
+ sources.push(typeof title === "string" && title.length > 0 ? { url, title } : { url });
36
+ };
37
+
38
+ let text = "";
39
+ let sawToolResultError = false;
40
+ if (!res.body) return { text: "", sources, error: "anthropic sidecar returned no response body" };
41
+
42
+ const decoder = new TextDecoder();
43
+ const reader = res.body.getReader();
44
+ let buffer = "";
45
+
46
+ const handleFrame = (data: Record<string, unknown>): void => {
47
+ const type = typeof data.type === "string" ? data.type : "";
48
+ if (type === "content_block_start") {
49
+ const block = isRec(data.content_block) ? data.content_block : {};
50
+ if (block.type === "web_search_tool_result") {
51
+ if (Array.isArray(block.content)) {
52
+ for (const hit of block.content) {
53
+ if (isRec(hit) && hit.type === "web_search_result") pushSource(hit.url, hit.title);
54
+ }
55
+ } else if (isRec(block.content) && block.content.type === "web_search_tool_result_error") {
56
+ sawToolResultError = true;
57
+ }
58
+ }
59
+ } else if (type === "content_block_delta") {
60
+ const delta = isRec(data.delta) ? data.delta : {};
61
+ if (delta.type === "text_delta" && typeof delta.text === "string") {
62
+ text += delta.text;
63
+ } else if (delta.type === "citations_delta") {
64
+ const citation = isRec(delta.citation) ? delta.citation : {};
65
+ if (citation.type === "web_search_result_location") pushSource(citation.url, citation.title);
66
+ }
67
+ }
68
+ };
69
+
70
+ // Parse one SSE frame's `data:` payload and fold it. Shared by the streaming loop and the EOF flush.
71
+ const processFrame = (rawFrame: string): void => {
72
+ let dataLine = "";
73
+ for (const line of rawFrame.split("\n")) {
74
+ if (line.startsWith("data:")) dataLine += line.slice(line.startsWith("data: ") ? 6 : 5);
75
+ }
76
+ if (!dataLine || dataLine === "[DONE]") return;
77
+ let data: unknown;
78
+ try { data = JSON.parse(dataLine); } catch { return; }
79
+ if (isRec(data)) handleFrame(data);
80
+ };
81
+
82
+ try {
83
+ for (;;) {
84
+ const { done, value } = await reader.read();
85
+ if (done) break;
86
+ // Normalize CRLF on the ACCUMULATED buffer so a `\r\n` pair split across two network chunks
87
+ // (chunk ends in `\r`, next starts with `\n`) still collapses to `\n` (audit round-2 F2).
88
+ buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
89
+ let sep: number;
90
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
91
+ const rawFrame = buffer.slice(0, sep);
92
+ buffer = buffer.slice(sep + 2);
93
+ processFrame(rawFrame);
94
+ }
95
+ }
96
+ // Flush the decoder and process any final unterminated frame (a stream that ends without \n\n).
97
+ buffer = (buffer + decoder.decode()).replace(/\r\n/g, "\n");
98
+ if (buffer.trim().length > 0) processFrame(buffer);
99
+ } catch {
100
+ /* mid-stream abort/decode failure: fall through with whatever text/sources were gathered */
101
+ }
102
+
103
+ const trimmed = text.trim();
104
+ if (trimmed.length === 0) {
105
+ return { text: "", sources, error: sawToolResultError ? "anthropic web search returned an error result" : "anthropic sidecar produced no answer" };
106
+ }
107
+ return { text: trimmed, sources };
108
+ }
109
+
110
+ /**
111
+ * Execute ONE web search via a Claude sidecar through the STORED anthropic OAuth credential — the
112
+ * Anthropic-backed analog of runWebSearch. Authenticates with getValidAccessToken (refresh handled)
113
+ * and reproduces the Claude Code OAuth fingerprint (identity system block first, oauth beta, client
114
+ * headers, stable session id) so the request is first-party-shaped. Never throws — returns `{error}`
115
+ * so the caller injects a graceful tool result.
116
+ */
117
+ export async function runAnthropicWebSearch(
118
+ query: string,
119
+ providerName: string,
120
+ provider: OcxProviderConfig,
121
+ settings: SidecarSettings,
122
+ abortSignal?: AbortSignal,
123
+ ): Promise<SidecarOutcome> {
124
+ const base = provider.baseUrl.replace(/\/v1\/?$/, "");
125
+ const url = `${base}/v1/messages`;
126
+ let token: string;
127
+ try {
128
+ token = await getValidAccessToken(providerName);
129
+ } catch (e) {
130
+ return { text: "", sources: [], error: `anthropic sidecar auth failed: ${e instanceof Error ? e.message : String(e)}` };
131
+ }
132
+ const headers: Record<string, string> = {
133
+ "Content-Type": "application/json",
134
+ "anthropic-version": "2023-06-01",
135
+ "Accept": "text/event-stream",
136
+ "User-Agent": "@anthropic-ai/sdk/0.74.0",
137
+ "Authorization": `Bearer ${token}`,
138
+ "anthropic-beta": ANTHROPIC_OAUTH_BETA,
139
+ ...CLAUDE_CODE_HEADERS,
140
+ "X-Claude-Code-Session-Id": claudeCodeSessionId(token),
141
+ "x-client-request-id": crypto.randomUUID(),
142
+ };
143
+ if (provider.headers) Object.assign(headers, provider.headers);
144
+
145
+ const instruction = settings.describeImages ? BASE_INSTRUCTION + IMAGE_INSTRUCTION : BASE_INSTRUCTION;
146
+ const body = {
147
+ model: settings.model,
148
+ max_tokens: ANTHROPIC_MAX_TOKENS,
149
+ // sonnet-5 defaults to adaptive thinking when omitted; keep the sidecar fast/cheap (audit F2).
150
+ thinking: { type: "disabled" },
151
+ // OAuth fingerprint requires the Claude Code identity as the FIRST system block (audit F6/anthropic.ts).
152
+ system: [
153
+ { type: "text", text: CLAUDE_CODE_SYSTEM_INSTRUCTION },
154
+ { type: "text", text: instruction },
155
+ ],
156
+ messages: [{ role: "user", content: [{ type: "text", text: query }] }],
157
+ tools: [{ type: "web_search_20250305", name: "web_search", max_uses: ANTHROPIC_MAX_USES }],
158
+ stream: true,
159
+ };
160
+
161
+ const linkedSignal = signalWithTimeout(settings.timeoutMs, abortSignal);
162
+ const sidecarExit = sidecarEnter("web-search");
163
+ const t0 = Date.now();
164
+ try {
165
+ const res = await fetchWithResetRetry(
166
+ () => fetch(url, { method: "POST", headers, body: JSON.stringify(body), signal: linkedSignal.signal }),
167
+ { abortSignal: linkedSignal.signal, label: "web-search-sidecar-anthropic" },
168
+ );
169
+ if (!res.ok) {
170
+ const t = await res.text().catch(() => "");
171
+ console.warn(`[web-search] anthropic sidecar HTTP ${res.status} for query "${query.slice(0, 80)}" (${Date.now() - t0}ms)`);
172
+ // Redact before surfacing: the body can echo auth headers/tokens (#398 review).
173
+ return { text: "", sources: [], error: `sidecar HTTP ${res.status}: ${redactSecretString(t.slice(0, 200))}` };
174
+ }
175
+ const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
176
+ try {
177
+ return await parseAnthropicSidecarSSE(res);
178
+ } finally {
179
+ detachBodyGuard();
180
+ }
181
+ } catch (e) {
182
+ const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
183
+ console.warn(`[web-search] anthropic sidecar ${kind} for query "${query.slice(0, 80)}" (${Date.now() - t0}ms)`);
184
+ return { text: "", sources: [], error: e instanceof Error ? e.message : String(e) };
185
+ } finally {
186
+ sidecarExit();
187
+ linkedSignal.cleanup();
188
+ }
189
+ }