@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,1046 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import type { ResponsesTerminalStatus } from "../bridge";
3
+ import {
4
+ classifyError,
5
+ httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError,
6
+ isClientClosedMessage,
7
+ } from "../lib/errors";
8
+ import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
9
+ import { readCodexCatalogPath } from "../codex/catalog";
10
+ import type { OcxUsage } from "../types";
11
+ import type { AdapterRequest } from "../adapters/base";
12
+ import { redactSecretString } from "../lib/redact";
13
+ import {
14
+ appendUsageEntry,
15
+ isKnownAdmissionKind,
16
+ isKnownInboundProtocol,
17
+ isKnownUsageSurface,
18
+ readRecentUsageEntries,
19
+ usageForFinalLog,
20
+ usageStatusForFinalLog,
21
+ usageTotalTokens,
22
+ type AttemptRecoveryKind,
23
+ type PersistedUsageAttempt,
24
+ type PersistedUsageEntry,
25
+ type UsageStatus,
26
+ } from "../usage/log";
27
+ import {
28
+ appendUsageDebug,
29
+ isUsageDebugEnabled,
30
+ truncateForDebug,
31
+ USAGE_DEBUG_BODY_SAMPLE_BYTES,
32
+ type UsageDebugBodyKind,
33
+ } from "../usage/debug";
34
+ import { matchesLogConversationId } from "./request-log-conversation";
35
+ import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
36
+
37
+ export interface RequestLogContext {
38
+ model: string;
39
+ provider: string;
40
+ /** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
41
+ firstOutputMs?: number;
42
+ /** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
43
+ conversationId?: string;
44
+ surface?: "claude" | "claude-desktop" | "grok";
45
+ /** The matched configured key's id. Set ONLY for admissionKind "configured" —
46
+ * never a sentinel, so a hand-edited entry whose id happens to be "loopback"
47
+ * cannot absorb unrelated traffic. */
48
+ apiKeyId?: string;
49
+ /** Which kind of admission opened this request. Carries no secret. */
50
+ admissionKind?: "configured" | "environment" | "loopback";
51
+ /** Which inbound wire was used. Orthogonal to `surface`, which names the client
52
+ * product: widening that enum would merge Responses and Chat Completions,
53
+ * since both leave it undefined. */
54
+ inboundProtocol?: "responses" | "chat" | "messages";
55
+ requestedModel?: string;
56
+ /** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
57
+ comboId?: string;
58
+ requestedEffort?: string;
59
+ effectiveEffort?: string;
60
+ reasoningWireField?: string;
61
+ reasoningWireValue?: string | number;
62
+ requestedServiceTier?: string;
63
+ requestedSpeedLabel?: string;
64
+ configuredServiceTier?: string;
65
+ configuredSpeedLabel?: string;
66
+ modelSupportsServiceTier?: boolean;
67
+ responseServiceTier?: string;
68
+ resolvedModel?: string;
69
+ usage?: OcxUsage;
70
+ usageLogInputTokens?: number;
71
+ attempts?: PersistedUsageAttempt[];
72
+ /** Internal mutable final attempt; omitted from RequestLogEntry/JSONL. */
73
+ activeAttempt?: PersistedUsageAttempt;
74
+ /** Internal wall-clock origin for the committed final attempt; never persisted. */
75
+ activeAttemptStartedAt?: number;
76
+ usageDebugBodyKind?: UsageDebugBodyKind;
77
+ usageDebugBodySample?: string;
78
+ usageDebugContentType?: string;
79
+ /** Route adapter type ("cursor"/"kiro"/"anthropic"/…): drives estimated-usage detection
80
+ * independent of the user-chosen provider NAME (devlog 130 B2). */
81
+ providerAdapter?: string;
82
+ /** Set when the bridge reported raw adapter usage via onUsage: the bridged wire now always
83
+ * carries synthetic zero-default token-detail objects (strict-client normalization, see
84
+ * responsesUsage in src/bridge.ts), so SSE/JSON re-parsing must not overwrite the raw
85
+ * provenance — a synthetic cached_tokens:0 is NOT a measured cache read. */
86
+ usageFromBridge?: boolean;
87
+ /** Secret-redacted upstream error reason (e.g. the granular Cursor "rate limit exceeded…"
88
+ * message) extracted from a `response.failed` SSE payload or non-streaming error body, so the
89
+ * request log / GUI shows the actual upstream failure rather than only the HTTP-mapped code. */
90
+ upstreamError?: string;
91
+ /** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
92
+ terminalHttpStatus?: number;
93
+ /** Structured reason from `response.incomplete`; internal-only input to log classification. */
94
+ terminalIncompleteReason?: string;
95
+ affinity?: "reused" | "new_bind" | "rebound" | "cleared";
96
+ transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
97
+ terminalSource?: "upstream" | "synthetic";
98
+ }
99
+
100
+ export interface RequestLogEntry {
101
+ requestId: string;
102
+ timestamp: number;
103
+ model: string;
104
+ provider: string;
105
+ /** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
106
+ firstOutputMs?: number;
107
+ surface?: "claude" | "claude-desktop" | "grok";
108
+ /** The matched configured key's id. Set ONLY for admissionKind "configured" —
109
+ * never a sentinel, so a hand-edited entry whose id happens to be "loopback"
110
+ * cannot absorb unrelated traffic. */
111
+ apiKeyId?: string;
112
+ /** Which kind of admission opened this request. Carries no secret. */
113
+ admissionKind?: "configured" | "environment" | "loopback";
114
+ /** Which inbound wire was used. Orthogonal to `surface`, which names the client
115
+ * product: widening that enum would merge Responses and Chat Completions,
116
+ * since both leave it undefined. */
117
+ inboundProtocol?: "responses" | "chat" | "messages";
118
+ /** Best-effort chat/session correlation for Logs grouping (#330). */
119
+ conversationId?: string;
120
+ requestedModel?: string;
121
+ requestedEffort?: string;
122
+ effectiveEffort?: string;
123
+ reasoningWireField?: string;
124
+ reasoningWireValue?: string | number;
125
+ requestedServiceTier?: string;
126
+ requestedSpeedLabel?: string;
127
+ configuredServiceTier?: string;
128
+ configuredSpeedLabel?: string;
129
+ modelSupportsServiceTier?: boolean;
130
+ responseServiceTier?: string;
131
+ resolvedModel?: string;
132
+ status: number;
133
+ durationMs: number;
134
+ errorCode?: string;
135
+ terminalStatus?: ResponsesTerminalStatus;
136
+ closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
137
+ /** Secret-redacted upstream error reason, surfaced in /api/logs and the GUI detail modal. */
138
+ upstreamError?: string;
139
+ usageStatus: UsageStatus;
140
+ usage?: OcxUsage;
141
+ totalTokens?: number;
142
+ attempts?: PersistedUsageAttempt[];
143
+ /** Codex pool affinity decision for this request (diagnostics for #186). */
144
+ affinity?: "reused" | "new_bind" | "rebound" | "cleared";
145
+ /** Where the upstream terminal/failure was observed. */
146
+ transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
147
+ /** Whether the terminal came from a real upstream SSE event or a proxy synthetic tail. */
148
+ terminalSource?: "upstream" | "synthetic";
149
+ }
150
+
151
+ const requestLog: RequestLogEntry[] = [];
152
+ const MAX_LOG_SIZE = 2000;
153
+ const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
154
+ let requestLogBytes = 0;
155
+ let requestLogSeq = 0;
156
+ /** True after hydrateRequestLogsFromDisk ran once in this process. */
157
+ let requestLogsHydratedFromDisk = false;
158
+
159
+ function retainedRequestLogBytes(entry: RequestLogEntry): number {
160
+ return Buffer.byteLength(JSON.stringify(entry), "utf8");
161
+ }
162
+
163
+ function removeOldestRequestLogEntry(): number {
164
+ const entry = requestLog.shift();
165
+ if (!entry) return 0;
166
+ const bytes = requestLogEntryBytes.get(entry) ?? retainedRequestLogBytes(entry);
167
+ requestLogEntryBytes.delete(entry);
168
+ requestLogBytes = Math.max(0, requestLogBytes - bytes);
169
+ return bytes;
170
+ }
171
+
172
+ function retainRequestLogEntry(entry: RequestLogEntry): void {
173
+ const bytes = retainedRequestLogBytes(entry);
174
+ requestLog.push(entry);
175
+ requestLogEntryBytes.set(entry, bytes);
176
+ requestLogBytes += bytes;
177
+ while (requestLog.length > MAX_LOG_SIZE) removeOldestRequestLogEntry();
178
+ enforceAppOwnedMemoryBudget();
179
+ }
180
+
181
+ export function requestLogRetainedStoreSnapshot(): RetainedStoreSnapshot {
182
+ return {
183
+ count: requestLog.length,
184
+ bytes: requestLogBytes,
185
+ evictableBytes: requestLogBytes,
186
+ pinnedBytes: 0,
187
+ oldestAt: requestLog[0]?.timestamp ?? null,
188
+ };
189
+ }
190
+
191
+ export function evictOldestRequestLogForBudget(): number {
192
+ return removeOldestRequestLogEntry();
193
+ }
194
+
195
+ function asTerminalStatus(value: string | undefined): ResponsesTerminalStatus | undefined {
196
+ if (value === "completed" || value === "failed" || value === "incomplete") return value;
197
+ return undefined;
198
+ }
199
+
200
+ function asCloseReason(value: string | undefined): RequestLogEntry["closeReason"] | undefined {
201
+ switch (value) {
202
+ case "terminal":
203
+ case "client_cancel":
204
+ case "non_stream":
205
+ case "body_stall":
206
+ case "body_overflow":
207
+ return value;
208
+ default:
209
+ return undefined;
210
+ }
211
+ }
212
+
213
+ /** Project a persisted usage.jsonl row back into the in-memory /api/logs shape. */
214
+ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): RequestLogEntry {
215
+ const terminalStatus = asTerminalStatus(entry.terminalStatus);
216
+ const closeReason = asCloseReason(entry.closeReason);
217
+ return {
218
+ requestId: entry.requestId,
219
+ timestamp: entry.timestamp,
220
+ model: entry.model,
221
+ provider: entry.provider,
222
+ ...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
223
+ ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
224
+ ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
225
+ ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
226
+ ...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
227
+ ...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
228
+ ...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
229
+ ...(entry.reasoningWireValue !== undefined ? { reasoningWireValue: entry.reasoningWireValue } : {}),
230
+ ...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
231
+ ...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
232
+ ...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
233
+ ...(entry.configuredSpeedLabel ? { configuredSpeedLabel: entry.configuredSpeedLabel } : {}),
234
+ ...(entry.modelSupportsServiceTier !== undefined
235
+ ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
236
+ : {}),
237
+ ...(entry.responseServiceTier ? { responseServiceTier: entry.responseServiceTier } : {}),
238
+ ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
239
+ status: entry.status,
240
+ durationMs: entry.durationMs,
241
+ ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
242
+ ...(terminalStatus ? { terminalStatus } : {}),
243
+ ...(closeReason ? { closeReason } : {}),
244
+ ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
245
+ usageStatus: entry.usageStatus,
246
+ ...(entry.usage ? { usage: entry.usage } : {}),
247
+ ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
248
+ ...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Seed the in-memory Logs ring buffer from usage.jsonl so GUI /api/logs survives
254
+ * `ocx stop` / `ocx start` (process restart). Idempotent per process; no-ops when
255
+ * the buffer already has live entries. Read failures are non-fatal (same as /api/usage).
256
+ */
257
+ export function hydrateRequestLogsFromDisk(
258
+ reader: () => PersistedUsageEntry[] = () => readRecentUsageEntries(MAX_LOG_SIZE),
259
+ ): number {
260
+ if (requestLogsHydratedFromDisk) return 0;
261
+ if (requestLog.length > 0) {
262
+ requestLogsHydratedFromDisk = true;
263
+ return 0;
264
+ }
265
+ try {
266
+ const persisted = reader();
267
+ requestLogsHydratedFromDisk = true;
268
+ if (persisted.length === 0) return 0;
269
+ const slice = persisted.length > MAX_LOG_SIZE
270
+ ? persisted.slice(persisted.length - MAX_LOG_SIZE)
271
+ : persisted;
272
+ for (const entry of slice) retainRequestLogEntry(requestLogEntryFromPersistedUsage(entry));
273
+ return slice.length;
274
+ } catch (err) {
275
+ requestLogsHydratedFromDisk = true;
276
+ console.warn(
277
+ `[request-log] failed to hydrate from usage.jsonl: ${err instanceof Error ? err.message : String(err)}`,
278
+ );
279
+ return 0;
280
+ }
281
+ }
282
+
283
+ export function addRequestLog(entry: RequestLogEntry) {
284
+ retainRequestLogEntry(entry);
285
+ try {
286
+ // Failure diagnostics survive the 200-entry ring buffer by riding the persisted
287
+ // usage entry (devlog/_plan/260716_claudecode_hardening/030). Success rows stay
288
+ // in their existing shape; the >=400 gate deliberately includes 499 client-cancels.
289
+ const failureDiagnostics = entry.status >= 400 || (entry.terminalStatus && entry.terminalStatus !== "completed")
290
+ ? {
291
+ ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
292
+ ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
293
+ ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
294
+ ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
295
+ }
296
+ : {};
297
+ appendUsageEntry({
298
+ requestId: entry.requestId,
299
+ timestamp: entry.timestamp,
300
+ provider: entry.provider,
301
+ model: entry.model,
302
+ ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
303
+ // This function REBUILDS the persisted row field by field rather than
304
+ // spreading it, so a field missing here reaches /api/logs and never
305
+ // reaches usage.jsonl — which is where the per-key rollup reads from.
306
+ ...(entry.apiKeyId ? { apiKeyId: entry.apiKeyId } : {}),
307
+ ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
308
+ ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
309
+ ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
310
+ ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
311
+ ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
312
+ ...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
313
+ ...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
314
+ ...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
315
+ ...(entry.reasoningWireValue !== undefined ? { reasoningWireValue: entry.reasoningWireValue } : {}),
316
+ ...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
317
+ ...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
318
+ ...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
319
+ ...(entry.configuredSpeedLabel ? { configuredSpeedLabel: entry.configuredSpeedLabel } : {}),
320
+ ...(entry.modelSupportsServiceTier !== undefined
321
+ ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
322
+ : {}),
323
+ ...(entry.responseServiceTier ? { responseServiceTier: entry.responseServiceTier } : {}),
324
+ status: entry.status,
325
+ durationMs: entry.durationMs,
326
+ ...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
327
+ usageStatus: entry.usageStatus,
328
+ ...(entry.usage ? { usage: entry.usage } : {}),
329
+ ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
330
+ ...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
331
+ ...failureDiagnostics,
332
+ });
333
+ } catch {
334
+ /* request logging must never fail a user request */
335
+ }
336
+ }
337
+
338
+ export function nextRequestLogId(timestamp = Date.now()): string {
339
+ requestLogSeq = (requestLogSeq % 1_000_000) + 1;
340
+ return `ocx-${timestamp.toString(36)}-${requestLogSeq.toString(36)}`;
341
+ }
342
+
343
+ /**
344
+ * One-shot TTFT recorder (WP4). Records the first non-empty model output moment
345
+ * relative to the request start, and — when a combo attempt is in flight —
346
+ * relative to that attempt's start as well. Later calls are no-ops, so both the
347
+ * bridge callback and the deferred SSE tap may fire without double-recording.
348
+ */
349
+ export function recordFirstOutput(
350
+ logCtx: RequestLogContext,
351
+ requestStartedAt: number,
352
+ now = Date.now(),
353
+ ): void {
354
+ if (!Number.isFinite(requestStartedAt) || !Number.isFinite(now)) return;
355
+ const requestElapsed = Math.max(0, now - requestStartedAt);
356
+ if (logCtx.firstOutputMs === undefined) logCtx.firstOutputMs = requestElapsed;
357
+ if (logCtx.activeAttempt && logCtx.activeAttempt.firstOutputMs === undefined) {
358
+ const attemptStartedAt = logCtx.activeAttemptStartedAt ?? requestStartedAt;
359
+ logCtx.activeAttempt.firstOutputMs = Math.max(0, now - attemptStartedAt);
360
+ }
361
+ }
362
+
363
+ /** Snapshot target-specific requested effort even for runTurn adapters with no AdapterRequest. */
364
+ export function recordAttemptRequestedEffort(logCtx: RequestLogContext): void {
365
+ const attempt = logCtx.activeAttempt;
366
+ if (!attempt) return;
367
+ delete attempt.requestedEffort;
368
+ try {
369
+ if (typeof logCtx.requestedEffort === "string" && logCtx.requestedEffort) {
370
+ attempt.requestedEffort = redactSecretString(logCtx.requestedEffort).slice(0, 64);
371
+ }
372
+ } catch {
373
+ // Request logging is best-effort and must not affect request delivery.
374
+ }
375
+ }
376
+
377
+ /** Copy the adapter's exact outbound reasoning parameter into the durable request log. */
378
+ export function recordAdapterReasoning(
379
+ logCtx: RequestLogContext,
380
+ request: AdapterRequest,
381
+ ): void {
382
+ delete logCtx.effectiveEffort;
383
+ delete logCtx.reasoningWireField;
384
+ delete logCtx.reasoningWireValue;
385
+ const attempt = logCtx.activeAttempt;
386
+ if (attempt) {
387
+ delete attempt.effectiveEffort;
388
+ delete attempt.reasoningWireField;
389
+ delete attempt.reasoningWireValue;
390
+ }
391
+ recordAttemptRequestedEffort(logCtx);
392
+
393
+ // Diagnostics must never make an otherwise valid upstream request fail. Config files
394
+ // written by older versions (or edited by hand) can contain values that violate the
395
+ // current TypeScript shape, so validate the runtime object before redacting strings.
396
+ try {
397
+ const raw: unknown = request.reasoningLog;
398
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
399
+ const reasoning = raw as Record<string, unknown>;
400
+ if (typeof reasoning.effectiveEffort !== "string" || !reasoning.effectiveEffort
401
+ || (reasoning.wireField !== "reasoning_effort"
402
+ && reasoning.wireField !== "thinking_budget"
403
+ && reasoning.wireField !== "thinking.type")
404
+ || (!(typeof reasoning.wireValue === "string" && reasoning.wireValue)
405
+ && !(typeof reasoning.wireValue === "number"
406
+ && Number.isFinite(reasoning.wireValue)
407
+ && reasoning.wireValue >= 0))) {
408
+ return;
409
+ }
410
+
411
+ const effectiveEffort = redactSecretString(reasoning.effectiveEffort).slice(0, 64);
412
+ const wireValue = typeof reasoning.wireValue === "string"
413
+ ? redactSecretString(reasoning.wireValue).slice(0, 64)
414
+ : reasoning.wireValue;
415
+ logCtx.effectiveEffort = effectiveEffort;
416
+ logCtx.reasoningWireField = reasoning.wireField;
417
+ logCtx.reasoningWireValue = wireValue;
418
+ if (attempt) {
419
+ attempt.effectiveEffort = effectiveEffort;
420
+ attempt.reasoningWireField = reasoning.wireField;
421
+ attempt.reasoningWireValue = wireValue;
422
+ }
423
+ } catch {
424
+ // Request logging is best-effort and must not affect request delivery.
425
+ }
426
+ }
427
+
428
+ export function requestLogErrorCode(status: number, upstreamError?: string): string | undefined {
429
+ if (status >= 200 && status < 400) return undefined;
430
+ // Defense in depth: mid-stream web-search aborts used to land as 502 with this message.
431
+ if (status === 499 || (upstreamError?.trim() && classifyError(status, "upstream_error", upstreamError).code === "client_closed_request")) {
432
+ return "client_closed_request";
433
+ }
434
+ if (status === 400 || status === 409) return "invalid_request_error";
435
+ if (status === 401) return "invalid_api_key";
436
+ if (status === 403) {
437
+ // Prefer message-aware codes (e.g. Ollama Cloud subscription gates) over a blunt
438
+ // invalid_api_key — 403 usually means authenticated but not allowed.
439
+ if (upstreamError?.trim()) {
440
+ const code = classifyError(403, "upstream_error", upstreamError).code;
441
+ if (code) return code;
442
+ }
443
+ return "permission_denied";
444
+ }
445
+ if (status === 429) return "rate_limit_exceeded";
446
+ if (status === 503) return "server_is_overloaded";
447
+ if (status >= 500) return "upstream_server_error";
448
+ return `http_${status}`;
449
+ }
450
+
451
+ export function requestLogSpeedLabel(serviceTier: string | undefined): string | undefined {
452
+ const normalized = serviceTier?.trim().toLowerCase();
453
+ if (normalized === "priority" || normalized === "fast") return "fast";
454
+ return undefined;
455
+ }
456
+
457
+ export function readConfiguredCodexServiceTier(): string | undefined {
458
+ try {
459
+ if (!existsSync(CODEX_CONFIG_PATH)) return undefined;
460
+ return readRootTomlString(readFileSync(CODEX_CONFIG_PATH, "utf-8"), "service_tier") ?? undefined;
461
+ } catch {
462
+ return undefined;
463
+ }
464
+ }
465
+
466
+ export function catalogModelSupportsServiceTier(modelId: string, serviceTier: string | undefined): boolean | undefined {
467
+ if (!serviceTier) return undefined;
468
+ const requestTier = serviceTier.trim().toLowerCase() === "fast" ? "priority" : serviceTier.trim();
469
+ try {
470
+ const catalogPath = readCodexCatalogPath();
471
+ if (!existsSync(catalogPath)) return undefined;
472
+ const catalog = JSON.parse(readFileSync(catalogPath, "utf-8")) as { models?: unknown };
473
+ const models = Array.isArray(catalog.models) ? catalog.models : [];
474
+ const entry = models.find(model => {
475
+ if (!model || typeof model !== "object") return false;
476
+ return (model as { slug?: unknown; id?: unknown }).slug === modelId
477
+ || (model as { slug?: unknown; id?: unknown }).id === modelId;
478
+ });
479
+ if (!entry || typeof entry !== "object") return undefined;
480
+ const tiers = (entry as { service_tiers?: unknown }).service_tiers;
481
+ return Array.isArray(tiers) && tiers.some(tier => (
482
+ tier && typeof tier === "object" && (tier as { id?: unknown }).id === requestTier
483
+ ));
484
+ } catch {
485
+ return undefined;
486
+ }
487
+ }
488
+
489
+ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unknown): void {
490
+ if (!payload || typeof payload !== "object") return;
491
+ const source = "response" in payload && typeof (payload as { response?: unknown }).response === "object"
492
+ ? (payload as { response?: unknown }).response
493
+ : payload;
494
+ if (!source || typeof source !== "object") return;
495
+ const model = (source as { model?: unknown }).model;
496
+ if (typeof model === "string" && model.trim()) logCtx.resolvedModel = model;
497
+ const serviceTier = (source as { service_tier?: unknown }).service_tier;
498
+ if (typeof serviceTier === "string" && serviceTier.trim()) logCtx.responseServiceTier = serviceTier;
499
+ const usage = usageFromResponsesPayload((source as { usage?: unknown }).usage);
500
+ if (usage && !logCtx.usageFromBridge) {
501
+ logCtx.usage = usage;
502
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
503
+ }
504
+ }
505
+
506
+ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined {
507
+ if (!usage || typeof usage !== "object") return undefined;
508
+ const raw = usage as {
509
+ input_tokens?: unknown;
510
+ output_tokens?: unknown;
511
+ input_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown };
512
+ output_tokens_details?: { reasoning_tokens?: unknown };
513
+ total_tokens?: unknown;
514
+ prompt_tokens?: unknown;
515
+ completion_tokens?: unknown;
516
+ prompt_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown };
517
+ completion_tokens_details?: { reasoning_tokens?: unknown };
518
+ };
519
+ if (typeof raw.input_tokens === "number" && typeof raw.output_tokens === "number") {
520
+ return {
521
+ inputTokens: raw.input_tokens,
522
+ outputTokens: raw.output_tokens,
523
+ ...(typeof raw.total_tokens === "number" ? { totalTokens: raw.total_tokens } : {}),
524
+ ...(typeof raw.input_tokens_details?.cached_tokens === "number"
525
+ ? {
526
+ cachedInputTokens: raw.input_tokens_details.cached_tokens,
527
+ cacheReadInputTokens: raw.input_tokens_details.cached_tokens,
528
+ }
529
+ : {}),
530
+ ...(typeof raw.input_tokens_details?.cache_write_tokens === "number"
531
+ ? { cacheCreationInputTokens: raw.input_tokens_details.cache_write_tokens }
532
+ : {}),
533
+ ...(typeof raw.output_tokens_details?.reasoning_tokens === "number"
534
+ ? { reasoningOutputTokens: raw.output_tokens_details.reasoning_tokens }
535
+ : {}),
536
+ };
537
+ }
538
+ if (typeof raw.prompt_tokens === "number" && typeof raw.completion_tokens === "number") {
539
+ return {
540
+ inputTokens: raw.prompt_tokens,
541
+ outputTokens: raw.completion_tokens,
542
+ ...(typeof raw.total_tokens === "number" ? { totalTokens: raw.total_tokens } : {}),
543
+ ...(typeof raw.prompt_tokens_details?.cached_tokens === "number"
544
+ ? {
545
+ cachedInputTokens: raw.prompt_tokens_details.cached_tokens,
546
+ cacheReadInputTokens: raw.prompt_tokens_details.cached_tokens,
547
+ }
548
+ : {}),
549
+ ...(typeof raw.prompt_tokens_details?.cache_write_tokens === "number"
550
+ ? { cacheCreationInputTokens: raw.prompt_tokens_details.cache_write_tokens }
551
+ : {}),
552
+ ...(typeof raw.completion_tokens_details?.reasoning_tokens === "number"
553
+ ? { reasoningOutputTokens: raw.completion_tokens_details.reasoning_tokens }
554
+ : {}),
555
+ };
556
+ }
557
+ return undefined;
558
+ }
559
+
560
+ export function inspectResponseLogJson(logCtx: RequestLogContext, text: string): void {
561
+ try {
562
+ applyResponseLogMetadata(logCtx, JSON.parse(text));
563
+ } catch {
564
+ /* body may not be JSON; request log metadata is best-effort only */
565
+ }
566
+ captureUpstreamError(logCtx, text);
567
+ if (isUsageDebugEnabled() && logCtx.usageDebugBodyKind === undefined) {
568
+ logCtx.usageDebugBodyKind = "json";
569
+ logCtx.usageDebugBodySample = truncateForDebug(text);
570
+ }
571
+ }
572
+
573
+ export function inspectResponseLogSsePayload(logCtx: RequestLogContext, payload: string | null): void {
574
+ if (!payload || payload.trim() === "[DONE]") return;
575
+ let parsed: unknown | undefined;
576
+ try {
577
+ parsed = JSON.parse(payload);
578
+ } catch {
579
+ /* SSE block payload may not be JSON; metadata inspection is best-effort */
580
+ }
581
+ inspectResponseLogSsePayloadParsed(logCtx, payload, parsed);
582
+ }
583
+
584
+ /** Inspect an SSE payload using the caller's single best-effort JSON parse. */
585
+ export function inspectResponseLogSsePayloadParsed(
586
+ logCtx: RequestLogContext,
587
+ payload: string | null,
588
+ parsed: unknown | undefined,
589
+ ): void {
590
+ if (!payload || payload.trim() === "[DONE]") return;
591
+ const debugEnabled = isUsageDebugEnabled();
592
+ const sseAlreadyMarked = logCtx.usageDebugBodyKind === "sse";
593
+ if (parsed !== undefined) applyResponseLogMetadata(logCtx, parsed);
594
+ captureUpstreamErrorParsed(logCtx, payload, parsed);
595
+ if (debugEnabled) {
596
+ if (!sseAlreadyMarked) {
597
+ logCtx.usageDebugBodyKind = "sse";
598
+ logCtx.usageDebugBodySample = truncateForDebug(payload);
599
+ } else if (typeof logCtx.usageDebugBodySample === "string"
600
+ && logCtx.usageDebugBodySample.length < USAGE_DEBUG_BODY_SAMPLE_BYTES) {
601
+ const combined = `${logCtx.usageDebugBodySample}\n${payload}`;
602
+ logCtx.usageDebugBodySample = truncateForDebug(combined);
603
+ }
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Capture the upstream error reason into the request log context. Codex/consumer surfaces only
609
+ * see an HTTP-mapped error code (502 → upstream_server_error); the granular reason lives inside
610
+ * a `response.failed` SSE payload's `error.message` (the adapter's redacted upstream message) or
611
+ * a non-streaming JSON error body. We keep the FIRST non-empty reason (the original failure) and
612
+ * run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
613
+ */
614
+ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
615
+ if (!text) return;
616
+ let parsed: unknown | undefined;
617
+ try {
618
+ parsed = JSON.parse(text);
619
+ } catch {
620
+ /* retain the raw malformed payload for the bounded fallback below */
621
+ }
622
+ captureUpstreamErrorParsed(logCtx, text, parsed);
623
+ }
624
+
625
+ function captureUpstreamErrorParsed(
626
+ logCtx: RequestLogContext,
627
+ text: string,
628
+ parsed: unknown | undefined,
629
+ ): void {
630
+ if (parsed !== undefined && parsed !== null) {
631
+ const json = parsed as {
632
+ type?: unknown;
633
+ error?: { message?: unknown };
634
+ last_error?: { message?: unknown };
635
+ response?: {
636
+ error?: { type?: unknown; code?: unknown; message?: unknown };
637
+ incomplete_details?: { reason?: unknown };
638
+ };
639
+ };
640
+ captureTerminalHttpStatus(logCtx, json);
641
+ const reason = json?.response?.incomplete_details?.reason;
642
+ if (json.type === "response.incomplete"
643
+ && logCtx.terminalIncompleteReason === undefined
644
+ && typeof reason === "string"
645
+ && reason.trim()) {
646
+ logCtx.terminalIncompleteReason = reason.trim();
647
+ }
648
+ if (logCtx.upstreamError) return;
649
+ const message = json?.error?.message
650
+ ?? json?.last_error?.message
651
+ ?? json?.response?.error?.message;
652
+ if (typeof message === "string" && message.trim()) {
653
+ logCtx.upstreamError = redactSecretString(message).slice(0, 500);
654
+ return;
655
+ }
656
+ // No human-readable error message: fall back to the structured incomplete reason emitted by
657
+ // the bridge on a stall-timeout or adapter EOF (response.incomplete). Maps the raw reason to a
658
+ // reader-facing label so a generic 502 in /api/logs explains WHY the turn ended, not just the
659
+ // mapped HTTP code.
660
+ if (typeof reason === "string" && reason.trim()) {
661
+ logCtx.upstreamError = redactSecretString(incompleteReasonLabel(reason.trim())).slice(0, 500);
662
+ }
663
+ return;
664
+ }
665
+ if (logCtx.upstreamError) return;
666
+ const trimmed = text.trim();
667
+ if (trimmed) {
668
+ logCtx.upstreamError = redactSecretString(trimmed).slice(0, 500);
669
+ }
670
+ }
671
+
672
+ /** Map a raw `incomplete_details.reason` (emitted by the bridge) to a reader-facing label. */
673
+ function incompleteReasonLabel(reason: string): string {
674
+ switch (reason) {
675
+ case "max_output_tokens":
676
+ return `Output reached the requested token limit (${reason})`;
677
+ case "upstream_stall_timeout":
678
+ return `Upstream stalled: no data for the stall-timeout window (${reason})`;
679
+ case "adapter_eof":
680
+ return `Upstream stream ended unexpectedly without a terminal event (${reason})`;
681
+ default:
682
+ return `Upstream incomplete: ${reason}`;
683
+ }
684
+ }
685
+
686
+ function captureTerminalHttpStatus(
687
+ logCtx: RequestLogContext,
688
+ json: {
689
+ type?: unknown;
690
+ response?: { error?: { type?: unknown; code?: unknown; message?: unknown } };
691
+ },
692
+ ): void {
693
+ if (logCtx.terminalHttpStatus !== undefined) return;
694
+ if (json.type !== "response.failed") return;
695
+ const error = json.response?.error;
696
+ if (!error || typeof error !== "object") return;
697
+ logCtx.terminalHttpStatus = httpStatusFromTerminalError({
698
+ type: typeof error.type === "string" ? error.type : undefined,
699
+ code: error.code === null || typeof error.code === "string" ? error.code : undefined,
700
+ message: typeof error.message === "string" ? error.message : undefined,
701
+ });
702
+ }
703
+
704
+ /** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
705
+ export function httpStatusFromTerminalError(error: {
706
+ type?: string;
707
+ code?: string | null;
708
+ message?: string;
709
+ } | undefined): number {
710
+ return httpStatusFromClassifiedTerminalError(error);
711
+ }
712
+
713
+ export function httpStatusForTerminalStatus(status: ResponsesTerminalStatus): number {
714
+ return status === "completed" ? 200 : 502;
715
+ }
716
+
717
+ export function httpStatusForRequestLogTerminal(
718
+ status: ResponsesTerminalStatus,
719
+ logCtx?: RequestLogContext,
720
+ ): number {
721
+ /**
722
+ * [Decision Log]
723
+ * - 목적과 의도: Keep request logs aligned with the successful HTTP/SSE contract.
724
+ * - 기존 구현 및 제약 조건: All incomplete terminals were recorded as 502 even when the
725
+ * client-requested output limit was reached normally.
726
+ * - 검토한 주요 대안: Treat every incomplete as success, or infer the reason from display text.
727
+ * - 선택한 방식: Only structured max_output_tokens incompletes map to 200.
728
+ * - 다른 대안 대신 이 방식을 선택한 이유: Stall, EOF, and unknown incompletes must remain
729
+ * visible failures, and display text is not a stable classification contract.
730
+ * - 장점, 단점 및 영향: Logs stop reporting false upstream errors while retaining the
731
+ * incomplete terminal detail; native callers without a structured reason keep old behavior.
732
+ */
733
+ if (status === "incomplete" && logCtx?.terminalIncompleteReason === "max_output_tokens") {
734
+ return 200;
735
+ }
736
+ if (status === "failed" && logCtx?.terminalHttpStatus !== undefined) {
737
+ return logCtx.terminalHttpStatus;
738
+ }
739
+ return httpStatusForTerminalStatus(status);
740
+ }
741
+
742
+ export function addFinalRequestLog(
743
+ requestId: string,
744
+ start: number,
745
+ logCtx: RequestLogContext,
746
+ status: number,
747
+ meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
748
+ addLog: (entry: RequestLogEntry) => void = addRequestLog,
749
+ ): void {
750
+ // Mid-stream web-search aborts used to emit response.failed and land as 502/upstream_server_error.
751
+ // Prefer the client-close classification whenever the captured reason says so.
752
+ const effectiveStatus = status >= 500 && logCtx.upstreamError && isClientClosedMessage(logCtx.upstreamError)
753
+ ? 499
754
+ : status;
755
+ const errorCode = requestLogErrorCode(effectiveStatus, logCtx.upstreamError);
756
+ // A response.failed whose classified status is 499 is still a client cancel, not an upstream
757
+ // terminal failure — keep /api/logs closeReason aligned with that.
758
+ const closeReason = effectiveStatus === 499
759
+ ? "client_cancel"
760
+ : meta?.closeReason;
761
+ if (logCtx.activeAttempt) {
762
+ finishRequestAttempt(
763
+ logCtx.activeAttempt,
764
+ effectiveStatus,
765
+ Date.now() - (logCtx.activeAttemptStartedAt ?? start),
766
+ logCtx.usage,
767
+ );
768
+ }
769
+ const existing = finalizedUsage(
770
+ logCtx.providerAdapter ?? logCtx.provider,
771
+ logCtx.usage,
772
+ logCtx.usageLogInputTokens,
773
+ );
774
+ const attempts = logCtx.attempts?.map(attempt => ({
775
+ ...attempt,
776
+ recoveryKinds: [...attempt.recoveryKinds],
777
+ ...(attempt.usage ? { usage: { ...attempt.usage } } : {}),
778
+ }));
779
+ const isCombo = logCtx.comboId !== undefined && (attempts?.length ?? 0) > 0;
780
+ const aggregate = isCombo ? aggregateAttemptUsage(attempts ?? []) : null;
781
+ const loggedUsage = aggregate?.usage ?? existing.usage;
782
+ const usageStatus = aggregate?.status ?? existing.status;
783
+ const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
784
+ addLog({
785
+ requestId,
786
+ timestamp: start,
787
+ model: isCombo ? logCtx.requestedModel! : logCtx.model,
788
+ provider: isCombo ? "combo" : logCtx.provider,
789
+ ...(logCtx.surface ? { surface: logCtx.surface } : {}),
790
+ ...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
791
+ ...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
792
+ ...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
793
+ ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
794
+ ...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
795
+ ...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
796
+ ...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
797
+ ...(logCtx.reasoningWireField ? { reasoningWireField: logCtx.reasoningWireField } : {}),
798
+ ...(logCtx.reasoningWireValue !== undefined ? { reasoningWireValue: logCtx.reasoningWireValue } : {}),
799
+ ...(logCtx.requestedServiceTier ? { requestedServiceTier: logCtx.requestedServiceTier } : {}),
800
+ ...(logCtx.requestedSpeedLabel ? { requestedSpeedLabel: logCtx.requestedSpeedLabel } : {}),
801
+ ...(logCtx.configuredServiceTier ? { configuredServiceTier: logCtx.configuredServiceTier } : {}),
802
+ ...(logCtx.configuredSpeedLabel ? { configuredSpeedLabel: logCtx.configuredSpeedLabel } : {}),
803
+ ...(logCtx.modelSupportsServiceTier !== undefined ? { modelSupportsServiceTier: logCtx.modelSupportsServiceTier } : {}),
804
+ ...(logCtx.responseServiceTier ? { responseServiceTier: logCtx.responseServiceTier } : {}),
805
+ ...(logCtx.resolvedModel ? { resolvedModel: logCtx.resolvedModel } : {}),
806
+ status: effectiveStatus,
807
+ durationMs: Date.now() - start,
808
+ ...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
809
+ ...(errorCode ? { errorCode } : {}),
810
+ ...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
811
+ ...(closeReason ? { closeReason } : {}),
812
+ ...(logCtx.upstreamError ? { upstreamError: logCtx.upstreamError } : {}),
813
+ usageStatus,
814
+ ...(loggedUsage ? { usage: loggedUsage } : {}),
815
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
816
+ ...(attempts?.length ? { attempts } : {}),
817
+ ...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
818
+ ...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
819
+ ...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
820
+ });
821
+ if (isUsageDebugEnabled()) {
822
+ appendUsageDebug({
823
+ ts: Date.now(),
824
+ requestId,
825
+ provider: logCtx.provider,
826
+ model: logCtx.model,
827
+ upstreamContentType: logCtx.usageDebugContentType ?? null,
828
+ upstreamStatus: effectiveStatus,
829
+ bodyKind: logCtx.usageDebugBodyKind ?? "none",
830
+ bodySample: logCtx.usageDebugBodySample ?? "",
831
+ extractedUsage: loggedUsage ?? null,
832
+ });
833
+ }
834
+ }
835
+
836
+ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchParams): RequestLogEntry[] {
837
+ let filtered = logs;
838
+ const provider = params.get("provider")?.trim();
839
+ if (provider) {
840
+ filtered = filtered.filter(entry => entry.provider === provider
841
+ || entry.attempts?.some(attempt => attempt.provider === provider));
842
+ }
843
+ const conversationId = params.get("conversationId")?.trim() || params.get("conversation")?.trim();
844
+ if (conversationId) {
845
+ filtered = filtered.filter(entry => matchesLogConversationId(entry.conversationId, conversationId));
846
+ }
847
+ const status = params.get("status")?.trim().toLowerCase();
848
+ if (status) {
849
+ filtered = /^[1-5]xx$/.test(status)
850
+ ? filtered.filter(entry => Math.floor(entry.status / 100) === Number(status[0]))
851
+ : filtered.filter(entry => String(entry.status) === status);
852
+ }
853
+ const tailRaw = params.get("tail")?.trim();
854
+ if (tailRaw) {
855
+ const tail = Number.parseInt(tailRaw, 10);
856
+ if (Number.isFinite(tail) && tail > 0) filtered = filtered.slice(-Math.min(tail, MAX_LOG_SIZE));
857
+ }
858
+ const offsetRaw = params.get("offset")?.trim();
859
+ const limitRaw = params.get("limit")?.trim();
860
+ if (limitRaw) {
861
+ const limit = Number.parseInt(limitRaw, 10);
862
+ const offset = offsetRaw ? Number.parseInt(offsetRaw, 10) : 0;
863
+ if (Number.isFinite(limit) && limit > 0) {
864
+ const capped = Math.min(limit, MAX_LOG_SIZE);
865
+ const startOffset = Number.isFinite(offset) && offset > 0 ? offset : 0;
866
+ const end = filtered.length - startOffset;
867
+ if (end <= 0) filtered = [];
868
+ else {
869
+ const begin = Math.max(0, end - capped);
870
+ filtered = filtered.slice(begin, end);
871
+ }
872
+ }
873
+ }
874
+ return filtered;
875
+ }
876
+
877
+ export function filteredRequestLogCount(logs: RequestLogEntry[], params: URLSearchParams): number {
878
+ const withoutPagination = new URLSearchParams(params);
879
+ withoutPagination.delete("limit");
880
+ withoutPagination.delete("offset");
881
+ return filterRequestLogs(logs, withoutPagination).length;
882
+ }
883
+
884
+ interface FinalizedUsageResult {
885
+ usage?: OcxUsage;
886
+ status: UsageStatus;
887
+ totalTokens?: number;
888
+ }
889
+
890
+ function finalizedUsage(
891
+ adapter: string,
892
+ usage: OcxUsage | undefined,
893
+ inputTokenEstimate: number | undefined,
894
+ ): FinalizedUsageResult {
895
+ const estimate = typeof inputTokenEstimate === "number"
896
+ && Number.isFinite(inputTokenEstimate)
897
+ && inputTokenEstimate >= 0
898
+ ? inputTokenEstimate
899
+ : undefined;
900
+ const finalUsage = usageForFinalLog(adapter, usage);
901
+ const usageFallback = !finalUsage && estimate !== undefined
902
+ ? { inputTokens: estimate, outputTokens: 0, estimated: true }
903
+ : undefined;
904
+ const loggedUsage = finalUsage && estimate !== undefined
905
+ ? {
906
+ ...finalUsage,
907
+ inputTokens: Math.max(finalUsage.inputTokens, estimate),
908
+ estimated: true,
909
+ }
910
+ : (finalUsage ?? usageFallback);
911
+ const totalTokens = usageTotalTokens(loggedUsage);
912
+ return {
913
+ status: usageStatusForFinalLog(loggedUsage),
914
+ ...(loggedUsage ? { usage: loggedUsage } : {}),
915
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
916
+ };
917
+ }
918
+
919
+ export function beginRequestAttempt(
920
+ ordinal: number,
921
+ provider: string,
922
+ model: string,
923
+ adapter: string,
924
+ ): PersistedUsageAttempt {
925
+ return {
926
+ ordinal,
927
+ provider,
928
+ model,
929
+ adapter,
930
+ status: 0,
931
+ durationMs: 0,
932
+ sendCount: 0,
933
+ recoveryKinds: [],
934
+ usageStatus: "unreported",
935
+ };
936
+ }
937
+
938
+ export function sealRequestAttemptIdentity(
939
+ attempt: PersistedUsageAttempt | undefined,
940
+ provider: string,
941
+ adapter: string,
942
+ ): void {
943
+ if (!attempt) return;
944
+ attempt.provider = provider;
945
+ attempt.adapter = adapter;
946
+ }
947
+
948
+ export function noteAttemptSend(
949
+ attempt: PersistedUsageAttempt | undefined,
950
+ inputTokenEstimate: number | undefined,
951
+ recovery?: AttemptRecoveryKind,
952
+ ): void {
953
+ if (!attempt) return;
954
+ attempt.sendCount += 1;
955
+ if (typeof inputTokenEstimate === "number"
956
+ && Number.isFinite(inputTokenEstimate)
957
+ && inputTokenEstimate >= 0) {
958
+ attempt.inputTokenEstimate = inputTokenEstimate;
959
+ }
960
+ if (recovery && !attempt.recoveryKinds.includes(recovery)) {
961
+ attempt.recoveryKinds.push(recovery);
962
+ }
963
+ }
964
+
965
+ export function finishRequestAttempt(
966
+ attempt: PersistedUsageAttempt,
967
+ status: number,
968
+ durationMs: number,
969
+ usage?: OcxUsage,
970
+ ): PersistedUsageAttempt {
971
+ const finalized = finalizedUsage(
972
+ attempt.adapter,
973
+ usage ?? attempt.usage,
974
+ attempt.inputTokenEstimate,
975
+ );
976
+ attempt.status = status;
977
+ attempt.durationMs = Math.max(0, durationMs);
978
+ attempt.usageStatus = finalized.status;
979
+ if (finalized.usage) attempt.usage = finalized.usage;
980
+ else delete attempt.usage;
981
+ if (finalized.totalTokens !== undefined) attempt.totalTokens = finalized.totalTokens;
982
+ else delete attempt.totalTokens;
983
+ const errorCode = requestLogErrorCode(status);
984
+ if (errorCode) attempt.errorCode = errorCode;
985
+ else delete attempt.errorCode;
986
+ return attempt;
987
+ }
988
+
989
+ export function aggregateAttemptUsage(
990
+ attempts: readonly PersistedUsageAttempt[],
991
+ ): FinalizedUsageResult {
992
+ const status: UsageStatus = attempts.length > 0
993
+ && attempts.every(attempt => attempt.usageStatus === "unsupported")
994
+ ? "unsupported"
995
+ : attempts.some(attempt => (
996
+ attempt.usageStatus === "unreported" || attempt.usageStatus === "unsupported"
997
+ ))
998
+ ? "unreported"
999
+ : attempts.some(attempt => attempt.usageStatus === "estimated")
1000
+ ? "estimated"
1001
+ : attempts.length > 0
1002
+ ? "reported"
1003
+ : "unreported";
1004
+
1005
+ const usages = attempts.flatMap(attempt => attempt.usage ? [attempt.usage] : []);
1006
+ if (usages.length === 0) return { status };
1007
+
1008
+ const sumOptional = (
1009
+ key: "cachedInputTokens" | "cacheReadInputTokens" | "cacheCreationInputTokens"
1010
+ | "reasoningOutputTokens",
1011
+ ): number | undefined => {
1012
+ const present = usages.flatMap(usage => (
1013
+ typeof usage[key] === "number" ? [usage[key] as number] : []
1014
+ ));
1015
+ return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : undefined;
1016
+ };
1017
+ const cachedInputTokens = sumOptional("cachedInputTokens");
1018
+ const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
1019
+ const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
1020
+ const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
1021
+ const totalTokens = usages.reduce(
1022
+ (sum, usage) => sum + (usageTotalTokens(usage) ?? 0),
1023
+ 0,
1024
+ );
1025
+ const aggregate: OcxUsage = {
1026
+ inputTokens: usages.reduce((sum, usage) => sum + usage.inputTokens, 0),
1027
+ outputTokens: usages.reduce((sum, usage) => sum + usage.outputTokens, 0),
1028
+ totalTokens,
1029
+ ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
1030
+ ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
1031
+ ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
1032
+ ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
1033
+ ...(status === "estimated" ? { estimated: true } : {}),
1034
+ };
1035
+ return { usage: aggregate, status, totalTokens };
1036
+ }
1037
+
1038
+ export function getRequestLogEntries(): RequestLogEntry[] { return requestLog; }
1039
+
1040
+ /** Test-only process-state reset for isolated integration harnesses. */
1041
+ export function clearRequestLogsForTests(): void {
1042
+ requestLog.length = 0;
1043
+ requestLogBytes = 0;
1044
+ requestLogSeq = 0;
1045
+ requestLogsHydratedFromDisk = false;
1046
+ }