mixdog 0.7.1

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 (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. package/tools.json +1653 -0
@@ -0,0 +1,1467 @@
1
+ import OpenAI from 'openai';
2
+ import { createHash } from 'crypto';
3
+ import { loadConfig } from '../config.mjs';
4
+ import { withRetry } from './retry-classifier.mjs';
5
+ import { sendViaWebSocket } from './openai-oauth-ws.mjs';
6
+ import { appendBridgeTrace, traceBridgeUsage } from '../bridge-trace.mjs';
7
+ import { resolveProviderCacheKey } from '../smart-bridge/cache-strategy.mjs';
8
+ import {
9
+ PROVIDER_FIRST_BYTE_TIMEOUT_MS,
10
+ PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
11
+ createTimeoutSignal,
12
+ resolveTimeoutMs,
13
+ } from '../stall-policy.mjs';
14
+ // OPENAI_COMPAT_PRESETS — self-declaring list of compat provider names and
15
+ // their base URLs / defaults. registry.mjs imports this export so there is no
16
+ // parallel OPENAI_COMPAT_PROVIDERS list to maintain separately.
17
+ export const OPENAI_COMPAT_PRESETS = {
18
+ deepseek: {
19
+ baseURL: 'https://api.deepseek.com',
20
+ defaultModel: 'deepseek-v4-pro',
21
+ },
22
+ xai: {
23
+ baseURL: 'https://api.x.ai/v1',
24
+ defaultModel: 'grok-4.3',
25
+ },
26
+ nvidia: {
27
+ baseURL: 'https://integrate.api.nvidia.com/v1',
28
+ defaultModel: 'meta/llama-3.3-70b-instruct',
29
+ },
30
+ ollama: {
31
+ baseURL: 'http://localhost:11434/v1',
32
+ defaultModel: 'llama3.3:latest',
33
+ },
34
+ lmstudio: {
35
+ baseURL: 'http://localhost:1234/v1',
36
+ defaultModel: 'default',
37
+ },
38
+ };
39
+ const PRESETS = OPENAI_COMPAT_PRESETS;
40
+ const MODEL_LIST_TIMEOUT_MS = resolveTimeoutMs(
41
+ 'MIXDOG_COMPAT_MODEL_LIST_TIMEOUT_MS',
42
+ 10_000,
43
+ { minMs: 1_000, maxMs: PROVIDER_GENERATE_TOTAL_TIMEOUT_MS },
44
+ );
45
+
46
+ // SSRF guard for provider baseURL. config.baseURL comes from user JSON;
47
+ // reject non-http(s) schemes (file:/data:/ftp:/etc.) and require https for
48
+ // any non-localhost host. Localhost-only presets (ollama, lmstudio) and
49
+ // loopback hosts may use http. Throws a clear config error — no silent
50
+ // fallback — so misconfig surfaces immediately instead of leaking apiKey.
51
+ function assertSafeBaseURL(rawURL, providerName) {
52
+ let parsed;
53
+ try {
54
+ parsed = new URL(String(rawURL));
55
+ } catch {
56
+ throw new Error(`[provider:${providerName}] invalid baseURL: ${rawURL}`);
57
+ }
58
+ const scheme = parsed.protocol.toLowerCase();
59
+ if (scheme !== 'https:' && scheme !== 'http:') {
60
+ throw new Error(`[provider:${providerName}] baseURL scheme not allowed: ${parsed.protocol} (only http/https)`);
61
+ }
62
+ if (scheme === 'http:') {
63
+ const host = parsed.hostname.toLowerCase();
64
+ const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
65
+ if (!isLocal) {
66
+ throw new Error(`[provider:${providerName}] baseURL must use https for non-localhost host (got ${parsed.protocol}//${parsed.hostname})`);
67
+ }
68
+ }
69
+ return rawURL;
70
+ }
71
+
72
+ function traceHash(value) {
73
+ return createHash('sha256')
74
+ .update(String(value ?? ''))
75
+ .digest('hex')
76
+ .slice(0, 16);
77
+ }
78
+
79
+ function stableTraceStringify(value, seen = new WeakSet()) {
80
+ if (value === null || typeof value !== 'object') {
81
+ if (typeof value === 'bigint') return JSON.stringify(String(value));
82
+ if (typeof value === 'undefined' || typeof value === 'function') return 'null';
83
+ return JSON.stringify(value);
84
+ }
85
+ if (seen.has(value)) return JSON.stringify('[Circular]');
86
+ seen.add(value);
87
+ if (Array.isArray(value)) {
88
+ const serialized = '[' + value.map(v => stableTraceStringify(v, seen)).join(',') + ']';
89
+ seen.delete(value);
90
+ return serialized;
91
+ }
92
+ const parts = [];
93
+ for (const key of Object.keys(value).sort()) {
94
+ const v = value[key];
95
+ if (typeof v === 'undefined' || typeof v === 'function') continue;
96
+ parts.push(JSON.stringify(key) + ':' + stableTraceStringify(v, seen));
97
+ }
98
+ seen.delete(value);
99
+ return '{' + parts.join(',') + '}';
100
+ }
101
+
102
+ function traceTextShape(text) {
103
+ const value = String(text ?? '');
104
+ return { chars: value.length, hash: traceHash(value) };
105
+ }
106
+
107
+ function summarizeTraceMessages(messages) {
108
+ const summaries = (messages || []).map((m, index) => {
109
+ const content = typeof m?.content === 'string'
110
+ ? { type: 'text', ...traceTextShape(m.content) }
111
+ : { type: m?.content == null ? 'null' : typeof m.content, hash: traceHash(stableTraceStringify(m?.content ?? null)) };
112
+ const toolCalls = Array.isArray(m?.tool_calls)
113
+ ? m.tool_calls.map(tc => ({
114
+ name: tc?.function?.name || null,
115
+ argsHash: traceHash(tc?.function?.arguments || ''),
116
+ }))
117
+ : [];
118
+ return {
119
+ index,
120
+ role: m?.role || null,
121
+ content,
122
+ ...(typeof m?.reasoning_content === 'string'
123
+ ? { reasoningContent: traceTextShape(m.reasoning_content) }
124
+ : {}),
125
+ toolCallCount: toolCalls.length,
126
+ ...(toolCalls.length ? { toolCalls } : {}),
127
+ };
128
+ });
129
+ if (summaries.length <= 12) return summaries;
130
+ return [
131
+ ...summaries.slice(0, 8),
132
+ { omittedTurns: summaries.length - 12 },
133
+ ...summaries.slice(-4),
134
+ ];
135
+ }
136
+
137
+ function summarizeTraceTools(tools) {
138
+ return (tools || []).map(t => ({
139
+ name: t?.name || null,
140
+ description: t?.description || '',
141
+ inputSchema: t?.inputSchema || null,
142
+ }));
143
+ }
144
+
145
+ function extractCompatCachedTokens(usage) {
146
+ const candidates = [
147
+ usage?.prompt_tokens_details?.cached_tokens,
148
+ usage?.input_tokens_details?.cached_tokens,
149
+ usage?.prompt_cache_hit_tokens,
150
+ usage?.cached_prompt_text_tokens,
151
+ ];
152
+ for (const v of candidates) {
153
+ const n = Number(v);
154
+ if (Number.isFinite(n) && n > 0) return n;
155
+ }
156
+ for (const v of candidates) {
157
+ const n = Number(v);
158
+ if (Number.isFinite(n)) return n;
159
+ }
160
+ return 0;
161
+ }
162
+
163
+ function xaiPrefixSeed({ opts, params, rawTools, model }) {
164
+ const providerKey = resolveProviderCacheKey(opts, 'xai');
165
+ const systemMessages = (params?.messages || [])
166
+ .filter(m => m?.role === 'system')
167
+ .map(m => String(m?.content ?? ''));
168
+ return stableTraceStringify({
169
+ scope: 'xai-prefix-model-system-tools',
170
+ providerKey: String(providerKey),
171
+ model: model || null,
172
+ systemMessages,
173
+ tools: summarizeTraceTools(rawTools),
174
+ });
175
+ }
176
+
177
+ function xaiCacheRouting(opts, params, rawTools, model) {
178
+ const sessionId = String(opts?.sessionId || opts?.session?.id || '').trim();
179
+ const providerKey = resolveProviderCacheKey(opts, 'xai');
180
+ const prefixSeed = xaiPrefixSeed({ opts, params, rawTools, model });
181
+ const prefixHash = traceHash(prefixSeed);
182
+ const routingSeed = stableTraceStringify({
183
+ scope: 'xai-chat-session-v1',
184
+ providerKey: String(providerKey),
185
+ model: model || null,
186
+ sessionId: sessionId || `ephemeral:${process.pid}`,
187
+ });
188
+ return {
189
+ key: deterministicUuidFromKey(routingSeed),
190
+ mode: sessionId ? 'session' : 'ephemeral',
191
+ seedHash: traceHash(routingSeed),
192
+ prefixHash,
193
+ ownerSessionHash: sessionId ? traceHash(sessionId) : null,
194
+ };
195
+ }
196
+
197
+ function xaiResponsesCacheRouting(opts, params, rawTools, model) {
198
+ // Default to 'prefix' so parallel workers sharing the same model + system
199
+ // + tools land on a common prompt_cache_key, letting xAI's server-side
200
+ // prefix cache hit across sessions instead of cold-starting per worker.
201
+ // Override with 'session' (env or opts) for legacy session-isolated lanes.
202
+ const scope = String(opts?.xaiResponsesCacheScope || process.env.MIXDOG_XAI_RESPONSES_CACHE_SCOPE || 'prefix')
203
+ .trim()
204
+ .toLowerCase();
205
+ if (scope !== 'prefix') {
206
+ return xaiCacheRouting(opts, params, rawTools, model);
207
+ }
208
+ const sessionId = String(opts?.sessionId || opts?.session?.id || '').trim();
209
+ const providerKey = resolveProviderCacheKey(opts, 'xai');
210
+ const prefixSeed = xaiPrefixSeed({ opts, params, rawTools, model });
211
+ const prefixHash = traceHash(prefixSeed);
212
+ const routingSeed = stableTraceStringify({
213
+ scope: 'xai-responses-prefix-v1',
214
+ providerKey: String(providerKey),
215
+ model: model || null,
216
+ prefixHash,
217
+ });
218
+ return {
219
+ key: deterministicUuidFromKey(routingSeed),
220
+ mode: 'prefix',
221
+ seedHash: traceHash(routingSeed),
222
+ prefixHash,
223
+ ownerSessionHash: sessionId ? traceHash(sessionId) : null,
224
+ };
225
+ }
226
+
227
+ function normalizeXaiReasoningEffort(value) {
228
+ const effort = String(value || '').trim().toLowerCase();
229
+ return ['none', 'low', 'medium', 'high'].includes(effort) ? effort : null;
230
+ }
231
+
232
+ function useXaiResponsesApi(opts, config) {
233
+ const raw = opts?.xaiApiMode
234
+ ?? config?.apiMode
235
+ ?? config?.xaiApiMode
236
+ ?? process.env.MIXDOG_XAI_API_MODE
237
+ ?? process.env.MIXDOG_XAI_RESPONSES;
238
+ if (raw == null || raw === '') return true;
239
+ const mode = String(raw).trim().toLowerCase();
240
+ return !['0', 'false', 'off', 'chat', 'chat-completions', 'chat_completions'].includes(mode);
241
+ }
242
+
243
+ function useXaiResponsesWebSocket(opts, config) {
244
+ const raw = opts?.xaiResponsesTransport
245
+ ?? opts?.xaiTransport
246
+ ?? config?.responsesTransport
247
+ ?? config?.transport
248
+ ?? process.env.MIXDOG_XAI_RESPONSES_TRANSPORT
249
+ ?? process.env.MIXDOG_XAI_TRANSPORT;
250
+ if (raw == null || raw === '') return true;
251
+ const transport = String(raw).trim().toLowerCase();
252
+ return !['0', 'false', 'off', 'http', 'https', 'responses-http', 'sdk'].includes(transport);
253
+ }
254
+
255
+ function useXaiResponsesWebSocketWarmup(opts, config, { previousResponseId, instructions, rawTools }) {
256
+ if (previousResponseId) return false;
257
+ const raw = opts?.xaiResponsesWarmup
258
+ ?? opts?.xaiWsWarmup
259
+ ?? config?.responsesWarmup
260
+ ?? config?.wsWarmup
261
+ ?? process.env.MIXDOG_XAI_RESPONSES_WARMUP
262
+ ?? process.env.MIXDOG_XAI_WS_WARMUP;
263
+ if (raw != null && raw !== '') {
264
+ const mode = String(raw).trim().toLowerCase();
265
+ if (['0', 'false', 'off', 'none', 'disabled'].includes(mode)) return false;
266
+ if (['1', 'true', 'on', 'always', 'force'].includes(mode)) return true;
267
+ }
268
+ return String(instructions || '').length >= 2048 || (Array.isArray(rawTools) && rawTools.length >= 10);
269
+ }
270
+
271
+ const XAI_RESPONSES_CACHE_LANE_DEFAULT_MAX_IN_FLIGHT = 2;
272
+ const xaiResponsesCacheLanes = new Map();
273
+
274
+ function parseXaiPositiveInt(value, fallback) {
275
+ if (value == null || value === '') return fallback;
276
+ const text = String(value).trim().toLowerCase();
277
+ if (['0', 'false', 'off', 'none', 'disabled', 'unlimited'].includes(text)) return 0;
278
+ const n = Number(text);
279
+ if (!Number.isFinite(n)) return fallback;
280
+ return Math.max(0, Math.floor(n));
281
+ }
282
+
283
+ function xaiResponsesCacheLaneMaxInFlight(opts, config) {
284
+ return parseXaiPositiveInt(
285
+ opts?.xaiCacheMaxInFlight
286
+ ?? opts?.xaiResponsesCacheMaxInFlight
287
+ ?? config?.xaiCacheMaxInFlight
288
+ ?? config?.xaiResponsesCacheMaxInFlight
289
+ ?? process.env.MIXDOG_XAI_CACHE_MAX_INFLIGHT
290
+ ?? process.env.MIXDOG_XAI_RESPONSES_CACHE_MAX_INFLIGHT,
291
+ XAI_RESPONSES_CACHE_LANE_DEFAULT_MAX_IN_FLIGHT,
292
+ );
293
+ }
294
+
295
+ function xaiResponsesCacheLaneQueueTimeoutMs(opts, config) {
296
+ return parseXaiPositiveInt(
297
+ opts?.xaiCacheQueueTimeoutMs
298
+ ?? opts?.xaiResponsesCacheQueueTimeoutMs
299
+ ?? config?.xaiCacheQueueTimeoutMs
300
+ ?? config?.xaiResponsesCacheQueueTimeoutMs
301
+ ?? process.env.MIXDOG_XAI_CACHE_QUEUE_TIMEOUT_MS
302
+ ?? process.env.MIXDOG_XAI_RESPONSES_CACHE_QUEUE_TIMEOUT_MS,
303
+ 0,
304
+ );
305
+ }
306
+
307
+ function xaiResponsesCacheLaneShard(opts, cacheRouting) {
308
+ const shardCount = parseXaiPositiveInt(
309
+ opts?.xaiCacheLaneShards
310
+ ?? opts?.xaiResponsesCacheLaneShards
311
+ ?? process.env.MIXDOG_XAI_CACHE_LANE_SHARDS
312
+ ?? process.env.MIXDOG_XAI_RESPONSES_CACHE_LANE_SHARDS,
313
+ 1,
314
+ );
315
+ if (shardCount <= 1) return 0;
316
+ const seed = String(opts?.sessionId || opts?.session?.id || cacheRouting?.ownerSessionHash || cacheRouting?.key || '');
317
+ const hex = traceHash(seed || 'xai-cache-lane');
318
+ return Number.parseInt(hex.slice(0, 8), 16) % shardCount;
319
+ }
320
+
321
+ function xaiResponsesCacheLaneKey({ model, cacheRouting, opts }) {
322
+ const prefix = cacheRouting?.prefixHash || cacheRouting?.seedHash || cacheRouting?.key || 'unknown-prefix';
323
+ const shard = xaiResponsesCacheLaneShard(opts, cacheRouting);
324
+ return {
325
+ key: `xai-responses:${model || 'default'}:${prefix}:shard-${shard}`,
326
+ shard,
327
+ };
328
+ }
329
+
330
+ function getXaiResponsesCacheLaneState(key, maxInFlight) {
331
+ let state = xaiResponsesCacheLanes.get(key);
332
+ if (!state) {
333
+ state = { key, active: 0, queue: [], maxInFlight, nextId: 0 };
334
+ xaiResponsesCacheLanes.set(key, state);
335
+ }
336
+ state.maxInFlight = maxInFlight;
337
+ return state;
338
+ }
339
+
340
+ function cleanupXaiResponsesCacheLane(state) {
341
+ if (state.active === 0 && state.queue.length === 0) {
342
+ xaiResponsesCacheLanes.delete(state.key);
343
+ }
344
+ }
345
+
346
+ function removeQueuedXaiCacheLaneRequest(state, request) {
347
+ const index = state.queue.indexOf(request);
348
+ if (index >= 0) state.queue.splice(index, 1);
349
+ cleanupXaiResponsesCacheLane(state);
350
+ }
351
+
352
+ function makeXaiCacheLaneHandle(state, requestId, enqueuedAt) {
353
+ let released = false;
354
+ return {
355
+ requestId,
356
+ waitedMs: Date.now() - enqueuedAt,
357
+ activeCount: state.active,
358
+ queueDepth: state.queue.length,
359
+ release() {
360
+ if (released) return;
361
+ released = true;
362
+ releaseXaiResponsesCacheLane(state);
363
+ },
364
+ };
365
+ }
366
+
367
+ function releaseXaiResponsesCacheLane(state) {
368
+ state.active = Math.max(0, state.active - 1);
369
+ while (state.queue.length > 0 && state.active < state.maxInFlight) {
370
+ const next = state.queue.shift();
371
+ next.cleanup?.();
372
+ state.active += 1;
373
+ next.resolve(makeXaiCacheLaneHandle(state, next.requestId, next.enqueuedAt));
374
+ }
375
+ cleanupXaiResponsesCacheLane(state);
376
+ }
377
+
378
+ function acquireXaiResponsesCacheLane({ key, maxInFlight, signal, timeoutMs }) {
379
+ const state = getXaiResponsesCacheLaneState(key, maxInFlight);
380
+ const requestId = ++state.nextId;
381
+ const enqueuedAt = Date.now();
382
+ if (state.active < state.maxInFlight) {
383
+ state.active += 1;
384
+ return Promise.resolve(makeXaiCacheLaneHandle(state, requestId, enqueuedAt));
385
+ }
386
+ return new Promise((resolve, reject) => {
387
+ const request = {
388
+ requestId,
389
+ enqueuedAt,
390
+ resolve,
391
+ reject,
392
+ cleanup: null,
393
+ };
394
+ const cleanup = () => {
395
+ if (request.timer) clearTimeout(request.timer);
396
+ if (signal && request.abortListener) signal.removeEventListener('abort', request.abortListener);
397
+ };
398
+ request.cleanup = cleanup;
399
+ request.abortListener = () => {
400
+ cleanup();
401
+ removeQueuedXaiCacheLaneRequest(state, request);
402
+ const reason = signal?.reason;
403
+ reject(reason instanceof Error ? reason : new Error('xAI cache lane wait aborted'));
404
+ };
405
+ if (signal?.aborted) {
406
+ request.abortListener();
407
+ return;
408
+ }
409
+ if (signal) signal.addEventListener('abort', request.abortListener, { once: true });
410
+ if (timeoutMs > 0) {
411
+ request.timer = setTimeout(() => {
412
+ cleanup();
413
+ removeQueuedXaiCacheLaneRequest(state, request);
414
+ reject(new Error(`xAI cache lane wait timed out after ${timeoutMs}ms`));
415
+ }, timeoutMs);
416
+ }
417
+ state.queue.push(request);
418
+ });
419
+ }
420
+
421
+ function traceXaiCacheLane(opts, payload) {
422
+ if (!compatCacheTraceEnabled('xai')) return;
423
+ try {
424
+ appendBridgeTrace({
425
+ sessionId: opts?.sessionId || opts?.session?.id || null,
426
+ iteration: Number.isFinite(Number(opts?.iteration)) ? Number(opts.iteration) : null,
427
+ kind: 'cache_lane',
428
+ ...payload,
429
+ payload,
430
+ });
431
+ } catch {}
432
+ }
433
+
434
+ async function withXaiResponsesCacheLane({ opts, config, cacheRouting, model, transport, previousResponseId, inputCount, signal }, fn) {
435
+ const maxInFlight = xaiResponsesCacheLaneMaxInFlight(opts, config);
436
+ if (maxInFlight <= 0) {
437
+ const laneMeta = { enabled: false, maxInFlight: 0 };
438
+ return { value: await fn(laneMeta), laneMeta };
439
+ }
440
+ const { key: laneKey, shard } = xaiResponsesCacheLaneKey({ model, cacheRouting, opts });
441
+ const timeoutMs = xaiResponsesCacheLaneQueueTimeoutMs(opts, config);
442
+ const state = getXaiResponsesCacheLaneState(laneKey, maxInFlight);
443
+ const queued = state.active >= state.maxInFlight;
444
+ if (queued) {
445
+ traceXaiCacheLane(opts, {
446
+ provider: 'xai',
447
+ api: 'responses',
448
+ transport,
449
+ event: 'queued',
450
+ lane_key_hash: traceHash(laneKey),
451
+ lane_shard: shard,
452
+ max_in_flight: maxInFlight,
453
+ active: state.active,
454
+ queue_depth: state.queue.length,
455
+ previous_response_used: !!previousResponseId,
456
+ input_count: inputCount,
457
+ });
458
+ }
459
+ const handle = await acquireXaiResponsesCacheLane({ key: laneKey, maxInFlight, signal, timeoutMs });
460
+ const laneMeta = {
461
+ enabled: true,
462
+ laneKeyHash: traceHash(laneKey),
463
+ shard,
464
+ maxInFlight,
465
+ queued,
466
+ waitMs: handle.waitedMs,
467
+ activeAfterAcquire: handle.activeCount,
468
+ queueDepthAfterAcquire: handle.queueDepth,
469
+ };
470
+ traceXaiCacheLane(opts, {
471
+ provider: 'xai',
472
+ api: 'responses',
473
+ transport,
474
+ event: 'acquired',
475
+ lane_key_hash: laneMeta.laneKeyHash,
476
+ lane_shard: shard,
477
+ max_in_flight: maxInFlight,
478
+ wait_ms: laneMeta.waitMs,
479
+ active: laneMeta.activeAfterAcquire,
480
+ queue_depth: laneMeta.queueDepthAfterAcquire,
481
+ previous_response_used: !!previousResponseId,
482
+ input_count: inputCount,
483
+ });
484
+ const startedAt = Date.now();
485
+ try {
486
+ return { value: await fn(laneMeta), laneMeta };
487
+ } finally {
488
+ handle.release();
489
+ traceXaiCacheLane(opts, {
490
+ provider: 'xai',
491
+ api: 'responses',
492
+ transport,
493
+ event: 'released',
494
+ lane_key_hash: laneMeta.laneKeyHash,
495
+ lane_shard: shard,
496
+ max_in_flight: maxInFlight,
497
+ held_ms: Date.now() - startedAt,
498
+ previous_response_used: !!previousResponseId,
499
+ input_count: inputCount,
500
+ });
501
+ }
502
+ }
503
+
504
+ function deterministicUuidFromKey(key) {
505
+ const hex = createHash('sha256').update(String(key ?? '')).digest('hex');
506
+ const variant = ((Number.parseInt(hex[16], 16) & 0x3) | 0x8).toString(16);
507
+ return [
508
+ hex.slice(0, 8),
509
+ hex.slice(8, 12),
510
+ '4' + hex.slice(13, 16),
511
+ variant + hex.slice(17, 20),
512
+ hex.slice(20, 32),
513
+ ].join('-');
514
+ }
515
+
516
+ function compatCacheTraceEnabled(provider) {
517
+ return process.env.MIXDOG_COMPAT_CACHE_TRACE === '1'
518
+ || process.env.MIXDOG_PROVIDER_CACHE_TRACE === '1'
519
+ || (provider === 'xai' && process.env.MIXDOG_XAI_CACHE_TRACE === '1');
520
+ }
521
+
522
+ function writeCompatCacheTrace({ provider, model, opts, params, rawTools, response, cacheRoutingKey, cacheRouting }) {
523
+ if (!compatCacheTraceEnabled(provider)) return;
524
+ try {
525
+ const usage = response?.usage || {};
526
+ const inputTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
527
+ const cachedTokens = extractCompatCachedTokens(usage);
528
+ const toolShape = summarizeTraceTools(rawTools);
529
+ const traceMessages = Array.isArray(params?.messages) ? params.messages : [];
530
+ const trace = {
531
+ event: 'chat.completions',
532
+ provider,
533
+ model,
534
+ responseModel: response?.model || null,
535
+ owner: opts?.session?.owner || null,
536
+ role: opts?.session?.role || opts?.role || null,
537
+ permission: opts?.session?.permission || null,
538
+ toolPermission: opts?.session?.toolPermission || null,
539
+ profileId: opts?.session?.profileId || null,
540
+ sourceType: opts?.session?.sourceType || null,
541
+ sourceName: opts?.session?.sourceName || null,
542
+ sessionIdHash: opts?.sessionId ? traceHash(opts.sessionId) : null,
543
+ providerCacheKeyHash: opts?.providerCacheKey ? traceHash(opts.providerCacheKey) : null,
544
+ promptCacheKeyHash: opts?.promptCacheKey ? traceHash(opts.promptCacheKey) : null,
545
+ xGrokConvIdHash: provider === 'xai' && cacheRoutingKey ? traceHash(cacheRoutingKey) : null,
546
+ xGrokConvIdSeedHash: provider === 'xai' ? cacheRouting?.seedHash || null : null,
547
+ xGrokPromptPrefixHash: provider === 'xai' ? cacheRouting?.prefixHash || null : null,
548
+ xGrokConvIdMode: provider === 'xai' ? cacheRouting?.mode || null : null,
549
+ xGrokConvIdLaneIndex: provider === 'xai' ? cacheRouting?.laneIndex ?? null : null,
550
+ xGrokConvIdActiveLanes: provider === 'xai' ? cacheRouting?.activeLanes ?? null : null,
551
+ xGrokConvIdIdleLanes: provider === 'xai' ? cacheRouting?.idleLanes ?? null : null,
552
+ xGrokConvIdOwnerSessionHash: provider === 'xai' ? cacheRouting?.ownerSessionHash || null : null,
553
+ xaiReasoningEffort: provider === 'xai' ? params?.reasoning_effort || null : null,
554
+ messageCount: traceMessages.length,
555
+ messageFullHash: traceHash(stableTraceStringify(traceMessages)),
556
+ messagePrefixHash: traceHash(stableTraceStringify(traceMessages.slice(0, -1))),
557
+ lastMessageHash: traceMessages.length ? traceHash(stableTraceStringify(traceMessages.at(-1))) : null,
558
+ messages: summarizeTraceMessages(traceMessages),
559
+ toolCount: Array.isArray(rawTools) ? rawTools.length : 0,
560
+ toolSchemaHash: traceHash(stableTraceStringify(toolShape)),
561
+ usageKeys: Object.keys(usage || {}).sort(),
562
+ promptTokenDetailsKeys: Object.keys(usage?.prompt_tokens_details || {}).sort(),
563
+ inputTokenDetailsKeys: Object.keys(usage?.input_tokens_details || {}).sort(),
564
+ choiceMessageKeys: Object.keys(response?.choices?.[0]?.message || {}).sort(),
565
+ responseReasoningContent: typeof response?.choices?.[0]?.message?.reasoning_content === 'string'
566
+ ? traceTextShape(response.choices[0].message.reasoning_content)
567
+ : null,
568
+ responseReasoningTokens: Number(usage?.completion_tokens_details?.reasoning_tokens ?? 0),
569
+ inputTokens,
570
+ outputTokens: Number(usage.completion_tokens ?? usage.output_tokens ?? 0),
571
+ cachedTokens,
572
+ cacheHitRate: inputTokens > 0 ? Number((cachedTokens / inputTokens).toFixed(6)) : null,
573
+ costInUsdTicks: typeof usage.cost_in_usd_ticks === 'number' ? usage.cost_in_usd_ticks : null,
574
+ };
575
+ process.stderr.write(`[compat-cache-trace] ${JSON.stringify(trace)}\n`);
576
+ } catch (err) {
577
+ process.stderr.write(`[compat-cache-trace] failed: ${err?.message || err}\n`);
578
+ }
579
+ }
580
+
581
+ function summarizeResponsesInput(input) {
582
+ return (input || []).map((item, index) => ({
583
+ index,
584
+ type: item?.type || null,
585
+ role: item?.role || null,
586
+ callIdHash: item?.call_id ? traceHash(item.call_id) : null,
587
+ name: item?.name || null,
588
+ content: typeof item?.content === 'string'
589
+ ? { type: 'text', ...traceTextShape(item.content) }
590
+ : { type: item?.content == null ? 'null' : typeof item.content, hash: traceHash(stableTraceStringify(item?.content ?? null)) },
591
+ output: typeof item?.output === 'string' ? traceTextShape(item.output) : null,
592
+ }));
593
+ }
594
+
595
+ function xaiUsageStats(usage) {
596
+ const inputTokens = Number(usage?.input_tokens ?? usage?.prompt_tokens ?? 0);
597
+ const outputTokens = Number(usage?.output_tokens ?? usage?.completion_tokens ?? 0);
598
+ const cachedTokens = extractCompatCachedTokens(usage);
599
+ const hitRate = inputTokens > 0 ? Number((cachedTokens / inputTokens).toFixed(6)) : null;
600
+ return { inputTokens, outputTokens, cachedTokens, hitRate };
601
+ }
602
+
603
+ function xaiSanitizedRequestSansInput(params) {
604
+ const { input: _input, ...rest } = params || {};
605
+ const out = { ...rest };
606
+ if (out.prompt_cache_key) out.prompt_cache_key = traceHash(out.prompt_cache_key);
607
+ if (out.previous_response_id) out.previous_response_id = traceHash(out.previous_response_id);
608
+ if (typeof out.instructions === 'string') out.instructions = traceHash(out.instructions);
609
+ return out;
610
+ }
611
+
612
+ function xaiResponsesFingerprintPayload({ model, opts, params, rawTools, response, cacheRouting, previousResponseId, inputStartIndex, transport, cacheLane }) {
613
+ const usage = response?.usage || {};
614
+ const { inputTokens, outputTokens, cachedTokens, hitRate } = xaiUsageStats(usage);
615
+ const toolShape = summarizeTraceTools(rawTools);
616
+ const instructions = typeof params?.instructions === 'string' ? params.instructions : '';
617
+ const requestSansInput = xaiSanitizedRequestSansInput(params);
618
+ const contextShape = {
619
+ provider: 'xai',
620
+ api: 'responses',
621
+ model: model || null,
622
+ promptCacheKeyHash: params?.prompt_cache_key ? traceHash(params.prompt_cache_key) : null,
623
+ instructions,
624
+ tools: toolShape,
625
+ reasoning: params?.reasoning || null,
626
+ store: params?.store ?? null,
627
+ };
628
+ const previousResponseUsed = Boolean(previousResponseId);
629
+ const midTurnCold = previousResponseUsed
630
+ && inputTokens >= 1024
631
+ && (cachedTokens <= 512 || (hitRate != null && hitRate < 0.1));
632
+ return {
633
+ provider: 'xai',
634
+ api: 'responses',
635
+ transport: transport || null,
636
+ model: model || null,
637
+ response_model: response?.model || null,
638
+ session_id_hash: opts?.sessionId ? traceHash(opts.sessionId) : null,
639
+ provider_cache_key_hash: opts?.providerCacheKey ? traceHash(opts.providerCacheKey) : null,
640
+ prompt_cache_key_option_hash: opts?.promptCacheKey ? traceHash(opts.promptCacheKey) : null,
641
+ prompt_cache_key_hash: params?.prompt_cache_key ? traceHash(params.prompt_cache_key) : null,
642
+ xai_prompt_prefix_hash: cacheRouting?.prefixHash || null,
643
+ xai_cache_mode: cacheRouting?.mode || null,
644
+ xai_cache_seed_hash: cacheRouting?.seedHash || null,
645
+ owner_session_hash: cacheRouting?.ownerSessionHash || null,
646
+ response_id_hash: response?.id ? traceHash(response.id) : null,
647
+ previous_response_id_hash: previousResponseId ? traceHash(previousResponseId) : null,
648
+ previous_response_used: previousResponseUsed,
649
+ input_start_index: inputStartIndex,
650
+ input_count: Array.isArray(params?.input) ? params.input.length : 0,
651
+ input_hash: traceHash(stableTraceStringify(params?.input || [])),
652
+ request_sans_input_hash: traceHash(stableTraceStringify(requestSansInput)),
653
+ context_prefix_hash: traceHash(stableTraceStringify(contextShape)),
654
+ has_instructions: instructions.length > 0,
655
+ instructions_chars: instructions.length,
656
+ instructions_hash: instructions ? traceHash(instructions) : null,
657
+ reasoning_effort: params?.reasoning?.effort || null,
658
+ tool_count: Array.isArray(rawTools) ? rawTools.length : 0,
659
+ tool_schema_hash: traceHash(stableTraceStringify(toolShape)),
660
+ tool_names_hash: traceHash(stableTraceStringify(toolShape.map(t => t?.name || null))),
661
+ xai_cache_lane_enabled: cacheLane?.enabled === true,
662
+ xai_cache_lane_hash: cacheLane?.laneKeyHash || null,
663
+ xai_cache_lane_shard: Number.isFinite(Number(cacheLane?.shard)) ? Number(cacheLane.shard) : null,
664
+ xai_cache_lane_max_in_flight: Number.isFinite(Number(cacheLane?.maxInFlight)) ? Number(cacheLane.maxInFlight) : null,
665
+ xai_cache_lane_wait_ms: Number.isFinite(Number(cacheLane?.waitMs)) ? Number(cacheLane.waitMs) : null,
666
+ xai_cache_lane_queued: cacheLane?.queued === true,
667
+ input_tokens: inputTokens,
668
+ output_tokens: outputTokens,
669
+ cached_tokens: cachedTokens,
670
+ cache_hit_rate: hitRate,
671
+ mid_turn_cold: midTurnCold,
672
+ };
673
+ }
674
+
675
+ function traceXaiResponsesCacheContext(args) {
676
+ if (!compatCacheTraceEnabled('xai')) return;
677
+ try {
678
+ const payload = xaiResponsesFingerprintPayload(args);
679
+ const sessionId = args?.opts?.sessionId || args?.opts?.session?.id || null;
680
+ const iteration = Number.isFinite(Number(args?.opts?.iteration)) ? Number(args.opts.iteration) : null;
681
+ appendBridgeTrace({
682
+ sessionId,
683
+ iteration,
684
+ kind: 'cache_context',
685
+ ...payload,
686
+ payload,
687
+ });
688
+ if (payload.mid_turn_cold) {
689
+ const anomalyPayload = {
690
+ ...payload,
691
+ anomaly: 'xai_mid_turn_cold_cache',
692
+ reason: 'previous_response_id_present_but_cached_tokens_low',
693
+ };
694
+ appendBridgeTrace({
695
+ sessionId,
696
+ iteration,
697
+ kind: 'cache_anomaly',
698
+ ...anomalyPayload,
699
+ payload: anomalyPayload,
700
+ });
701
+ }
702
+ } catch (err) {
703
+ process.stderr.write(`[compat-cache-trace] xai context trace failed: ${err?.message || err}\n`);
704
+ }
705
+ }
706
+
707
+ function writeXaiResponsesCacheTrace({ model, opts, params, rawTools, response, cacheRouting, previousResponseId, inputStartIndex, transport, cacheLane }) {
708
+ if (!compatCacheTraceEnabled('xai')) return;
709
+ try {
710
+ const usage = response?.usage || {};
711
+ const fingerprint = xaiResponsesFingerprintPayload({
712
+ model,
713
+ opts,
714
+ params,
715
+ rawTools,
716
+ response,
717
+ cacheRouting,
718
+ previousResponseId,
719
+ inputStartIndex,
720
+ transport,
721
+ cacheLane,
722
+ });
723
+ const inputTokens = fingerprint.input_tokens;
724
+ const cachedTokens = fingerprint.cached_tokens;
725
+ const toolShape = summarizeTraceTools(rawTools);
726
+ const trace = {
727
+ event: 'responses',
728
+ provider: 'xai',
729
+ transport: transport || null,
730
+ model,
731
+ responseModel: response?.model || null,
732
+ responseIdHash: response?.id ? traceHash(response.id) : null,
733
+ previousResponseIdHash: previousResponseId ? traceHash(previousResponseId) : null,
734
+ owner: opts?.session?.owner || null,
735
+ role: opts?.session?.role || opts?.role || null,
736
+ permission: opts?.session?.permission || null,
737
+ toolPermission: opts?.session?.toolPermission || null,
738
+ profileId: opts?.session?.profileId || null,
739
+ sourceType: opts?.session?.sourceType || null,
740
+ sourceName: opts?.session?.sourceName || null,
741
+ sessionIdHash: opts?.sessionId ? traceHash(opts.sessionId) : null,
742
+ promptCacheKeyHash: params?.prompt_cache_key ? traceHash(params.prompt_cache_key) : null,
743
+ xGrokPromptPrefixHash: cacheRouting?.prefixHash || null,
744
+ xGrokConvIdMode: cacheRouting?.mode || null,
745
+ xaiReasoningEffort: params?.reasoning?.effort || null,
746
+ previousResponseUsed: Boolean(previousResponseId),
747
+ inputStartIndex,
748
+ inputCount: Array.isArray(params?.input) ? params.input.length : 0,
749
+ cacheLaneEnabled: fingerprint.xai_cache_lane_enabled,
750
+ cacheLaneHash: fingerprint.xai_cache_lane_hash,
751
+ cacheLaneShard: fingerprint.xai_cache_lane_shard,
752
+ cacheLaneMaxInFlight: fingerprint.xai_cache_lane_max_in_flight,
753
+ cacheLaneWaitMs: fingerprint.xai_cache_lane_wait_ms,
754
+ cacheLaneQueued: fingerprint.xai_cache_lane_queued,
755
+ input: summarizeResponsesInput(params?.input || []),
756
+ toolCount: Array.isArray(rawTools) ? rawTools.length : 0,
757
+ toolSchemaHash: traceHash(stableTraceStringify(toolShape)),
758
+ toolNamesHash: fingerprint.tool_names_hash,
759
+ requestSansInputHash: fingerprint.request_sans_input_hash,
760
+ contextPrefixHash: fingerprint.context_prefix_hash,
761
+ instructionsHash: fingerprint.instructions_hash,
762
+ instructionsChars: fingerprint.instructions_chars,
763
+ usageKeys: Object.keys(usage || {}).sort(),
764
+ inputTokenDetailsKeys: Object.keys(usage?.input_tokens_details || {}).sort(),
765
+ outputTokenDetailsKeys: Object.keys(usage?.output_tokens_details || {}).sort(),
766
+ outputTypes: (response?.output || []).map(item => item?.type || null),
767
+ inputTokens,
768
+ outputTokens: fingerprint.output_tokens,
769
+ cachedTokens,
770
+ cacheHitRate: fingerprint.cache_hit_rate,
771
+ midTurnCold: fingerprint.mid_turn_cold,
772
+ costInUsdTicks: typeof usage.cost_in_usd_ticks === 'number' ? usage.cost_in_usd_ticks : null,
773
+ };
774
+ process.stderr.write(`[compat-cache-trace] ${JSON.stringify(trace)}\n`);
775
+ } catch (err) {
776
+ process.stderr.write(`[compat-cache-trace] failed: ${err?.message || err}\n`);
777
+ }
778
+ }
779
+
780
+ function toOpenAIMessages(messages, providerName) {
781
+ // NOTE: chat.completions has no equivalent slot for replaying reasoning
782
+ // encrypted_content the way the Responses API does (no `type:'reasoning'`
783
+ // input item). Whatever reasoningItems may be attached to assistant
784
+ // messages by the openai-oauth provider is intentionally dropped here —
785
+ // strict providers (xai) reject unknown roles/types and would 400 the
786
+ // request. Documented in v0.1.160 (GPT reasoning replay).
787
+ //
788
+ // DeepSeek thinking models require the prior turn's `reasoning_content`
789
+ // string to be echoed back inside the assistant message, otherwise the API
790
+ // returns 400. xAI reasoning models also preserve their official multi-turn
791
+ // shape and cache prefix stability when prior assistant reasoning_content
792
+ // is replayed; reasoning_effort itself remains caller/user-selected.
793
+ const replaysReasoningContent = providerName === 'deepseek' || providerName === 'xai';
794
+ const out = [];
795
+ for (const m of messages) {
796
+ if (m.role === 'tool') {
797
+ out.push({
798
+ role: 'tool',
799
+ tool_call_id: m.toolCallId || '',
800
+ content: m.content,
801
+ });
802
+ continue;
803
+ }
804
+ if (m.role === 'assistant' && m.toolCalls?.length) {
805
+ const msg = {
806
+ role: 'assistant',
807
+ content: m.content || null,
808
+ tool_calls: m.toolCalls.map((tc) => ({
809
+ id: tc.id,
810
+ type: 'function',
811
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
812
+ })),
813
+ };
814
+ if (replaysReasoningContent && m.reasoningContent) msg.reasoning_content = m.reasoningContent;
815
+ out.push(msg);
816
+ continue;
817
+ }
818
+ if (m.role === 'assistant' && replaysReasoningContent && m.reasoningContent) {
819
+ out.push({ role: m.role, content: m.content, reasoning_content: m.reasoningContent });
820
+ continue;
821
+ }
822
+ out.push({ role: m.role, content: m.content });
823
+ }
824
+ return out;
825
+ }
826
+ function toOpenAITools(tools) {
827
+ return tools.map((t) => ({
828
+ type: 'function',
829
+ function: {
830
+ name: t.name,
831
+ description: t.description,
832
+ parameters: t.inputSchema,
833
+ },
834
+ }));
835
+ }
836
+ function toResponsesTools(tools) {
837
+ return tools.map((t) => ({
838
+ type: 'function',
839
+ name: t.name,
840
+ description: t.description,
841
+ parameters: t.inputSchema,
842
+ }));
843
+ }
844
+ function parseToolCalls(choice) {
845
+ const calls = choice.message?.tool_calls;
846
+ if (!calls?.length)
847
+ return undefined;
848
+ return calls
849
+ .filter((tc) => tc.type === 'function')
850
+ .map((tc) => ({
851
+ id: tc.id,
852
+ name: tc.function.name,
853
+ arguments: JSON.parse(tc.function.arguments || '{}'),
854
+ }));
855
+ }
856
+ function parseJsonObject(value) {
857
+ try {
858
+ const parsed = JSON.parse(value || '{}');
859
+ return parsed && typeof parsed === 'object' ? parsed : {};
860
+ } catch {
861
+ return {};
862
+ }
863
+ }
864
+ function parseResponsesToolCalls(response) {
865
+ const out = [];
866
+ for (const item of response?.output || []) {
867
+ if (item?.type !== 'function_call') continue;
868
+ out.push({
869
+ id: item.call_id || item.id,
870
+ name: item.name,
871
+ arguments: parseJsonObject(item.arguments),
872
+ });
873
+ }
874
+ return out.length ? out : undefined;
875
+ }
876
+ function responseOutputText(response) {
877
+ if (typeof response?.output_text === 'string') return response.output_text;
878
+ const chunks = [];
879
+ for (const item of response?.output || []) {
880
+ if (item?.type !== 'message' || !Array.isArray(item.content)) continue;
881
+ for (const part of item.content) {
882
+ if (part?.type === 'output_text' && typeof part.text === 'string') chunks.push(part.text);
883
+ }
884
+ }
885
+ return chunks.join('');
886
+ }
887
+ function toResponsesInputMessage(m) {
888
+ if (m.role === 'tool') {
889
+ return {
890
+ type: 'function_call_output',
891
+ call_id: m.toolCallId || '',
892
+ output: typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''),
893
+ };
894
+ }
895
+ if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
896
+ const items = [];
897
+ if (m.content) items.push({ role: 'assistant', content: m.content });
898
+ for (const tc of m.toolCalls) {
899
+ items.push({
900
+ type: 'function_call',
901
+ call_id: tc.id,
902
+ name: tc.name,
903
+ arguments: JSON.stringify(tc.arguments || {}),
904
+ });
905
+ }
906
+ return items;
907
+ }
908
+ return { role: m.role, content: m.content || '' };
909
+ }
910
+ function xaiSystemInstructions(messages) {
911
+ const instructions = (messages || [])
912
+ .filter(m => m?.role === 'system')
913
+ .map(m => String(m.content || ''))
914
+ .filter(Boolean)
915
+ .join('\n\n');
916
+ return instructions || undefined;
917
+ }
918
+ function toXaiResponsesInput(messages, providerState, options = {}) {
919
+ const includeSystem = options.includeSystem !== false;
920
+ const state = providerState?.xaiResponses || null;
921
+ let startIndex = 0;
922
+ const previousResponseId = typeof state?.previousResponseId === 'string' ? state.previousResponseId : null;
923
+ if (previousResponseId) {
924
+ const seen = Number.isInteger(state?.seenMessageCount) ? state.seenMessageCount : messages.length;
925
+ startIndex = Math.max(0, Math.min(seen, messages.length));
926
+ if (messages[startIndex]?.role === 'assistant') startIndex += 1;
927
+ }
928
+ const input = [];
929
+ for (const m of messages.slice(startIndex)) {
930
+ if (!includeSystem && m.role === 'system') continue;
931
+ const converted = toResponsesInputMessage(m);
932
+ if (Array.isArray(converted)) input.push(...converted);
933
+ else input.push(converted);
934
+ }
935
+ return { input, previousResponseId, startIndex };
936
+ }
937
+ export class OpenAICompatProvider {
938
+ // Chat Completions prompt_tokens is already the total (includes cached).
939
+ // Covers grok-oauth and all OPENAI_COMPAT_PRESETS. See registry.mjs.
940
+ static inputExcludesCache = false;
941
+ name;
942
+ client;
943
+ defaultModel;
944
+ config;
945
+ baseURL;
946
+ apiKey;
947
+ defaultHeaders;
948
+ constructor(name, config) {
949
+ const preset = PRESETS[name];
950
+ const baseURL = assertSafeBaseURL(config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', name);
951
+ const apiKey = config.apiKey || 'no-key';
952
+ this.name = name;
953
+ this.config = config;
954
+ this.baseURL = baseURL;
955
+ this.apiKey = apiKey;
956
+ // Merge caller-supplied headers (config.extraHeaders) over the preset's.
957
+ // Used e.g. by grok-oauth to inject the Grok CLI client headers for the
958
+ // grok-build proxy. Backward-compatible: providers that pass no
959
+ // extraHeaders behave exactly as before.
960
+ this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(config.extraHeaders || {}) };
961
+ this.defaultModel = preset?.defaultModel || 'default';
962
+ this.client = new OpenAI({
963
+ baseURL,
964
+ apiKey,
965
+ defaultHeaders: this.defaultHeaders,
966
+ });
967
+ }
968
+ reloadApiKey() {
969
+ try {
970
+ const freshConfig = loadConfig();
971
+ const cfg = freshConfig.providers?.[this.name];
972
+ const preset = PRESETS[this.name];
973
+ const newKey = cfg?.apiKey || this.config.apiKey;
974
+ const baseURL = assertSafeBaseURL(cfg?.baseURL || this.config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', this.name);
975
+ if (newKey) {
976
+ this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey, baseURL };
977
+ this.baseURL = baseURL;
978
+ this.apiKey = newKey;
979
+ this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(this.config.extraHeaders || {}) };
980
+ this.client = new OpenAI({
981
+ baseURL,
982
+ apiKey: newKey,
983
+ defaultHeaders: this.defaultHeaders,
984
+ });
985
+ }
986
+ } catch { /* best effort */ }
987
+ }
988
+ async send(messages, model, tools, sendOpts) {
989
+ try {
990
+ return await this._doSend(messages, model, tools, sendOpts);
991
+ } catch (err) {
992
+ if (err.message && (err.message.includes('401') || err.message.includes('403'))) {
993
+ process.stderr.write(`[provider] Auth error, re-reading config...\n`);
994
+ this.reloadApiKey();
995
+ return await this._doSend(messages, model, tools, sendOpts);
996
+ }
997
+ throw err;
998
+ }
999
+ }
1000
+ async _doSend(messages, model, tools, sendOpts) {
1001
+ const useModel = model || this.defaultModel;
1002
+ const opts = sendOpts || {};
1003
+ if (this.name === 'xai' && useXaiResponsesApi(opts, this.config)) {
1004
+ if (useXaiResponsesWebSocket(opts, this.config)) {
1005
+ return await this._doSendXaiResponsesWebSocket(messages, useModel, tools, opts);
1006
+ }
1007
+ return await this._doSendXaiResponses(messages, useModel, tools, opts);
1008
+ }
1009
+ const signal = opts.signal || null;
1010
+ if (signal?.aborted) {
1011
+ const reason = signal.reason;
1012
+ throw reason instanceof Error ? reason : new Error('OpenAI-compat request aborted by session close');
1013
+ }
1014
+ const params = {
1015
+ model: useModel,
1016
+ messages: toOpenAIMessages(messages, this.name),
1017
+ };
1018
+ if (tools?.length) {
1019
+ params.tools = toOpenAITools(tools);
1020
+ }
1021
+ if (this.name === 'xai') {
1022
+ const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
1023
+ ?? opts.effort
1024
+ ?? this.config?.reasoningEffort
1025
+ ?? process.env.MIXDOG_XAI_REASONING_EFFORT);
1026
+ if (reasoningEffort) params.reasoning_effort = reasoningEffort;
1027
+ }
1028
+ const totalSignal = createTimeoutSignal(signal, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, `${this.name} total`);
1029
+ const cacheRouting = this.name === 'xai'
1030
+ ? xaiCacheRouting(opts, params, tools || [], useModel)
1031
+ : null;
1032
+ const cacheRoutingKey = cacheRouting?.key || null;
1033
+ // Note: x-grok-conv-id is documented as a routing hint, but in our
1034
+ // measured parallel-worker traffic it caused alternating cold caches
1035
+ // (server-side per-conv shard isolation). Vercel ai-sdk and other
1036
+ // reference clients omit it entirely and rely on xAI's automatic
1037
+ // prompt-prefix caching, which holds up to 95%+ hit even across
1038
+ // parallel workers. Keep the header off by default.
1039
+ // Shared retry: deepseek / xai / other compat backends all sit behind
1040
+ // their own load balancers and emit 5xx / "overloaded" under burst
1041
+ // traffic. The withRetry wrapper preserves abort behavior via
1042
+ // mergedSignal and only retries when classifyError() says transient.
1043
+ let response;
1044
+ try {
1045
+ response = await withRetry(
1046
+ ({ signal: attemptSignal }) => this.client.chat.completions.create(params, { signal: attemptSignal }),
1047
+ {
1048
+ signal: totalSignal.signal,
1049
+ perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
1050
+ perAttemptLabel: `${this.name} first byte`,
1051
+ onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
1052
+ const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
1053
+ process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
1054
+ },
1055
+ },
1056
+ );
1057
+ } finally {
1058
+ totalSignal.cleanup();
1059
+ }
1060
+ const choice = response.choices[0];
1061
+ const toolCalls = choice ? parseToolCalls(choice) : undefined;
1062
+ // Capture finish_reason early so we can refuse to return an
1063
+ // incomplete completion as final content. OpenAI-compat backends use
1064
+ // `length` (max_tokens / model context overflow) and `content_filter`
1065
+ // (moderation cutoff) to flag responses that were terminated before
1066
+ // the model finished its turn — treating those as success silently
1067
+ // surfaces truncated text and lets the loop accept a partial answer.
1068
+ const stopReason = choice?.finish_reason || null;
1069
+ if (stopReason === 'length' || stopReason === 'content_filter') {
1070
+ const err = Object.assign(
1071
+ new Error(`${this.name} response incomplete: finish_reason=${stopReason}`),
1072
+ {
1073
+ name: 'ProviderIncompleteError',
1074
+ code: 'PROVIDER_INCOMPLETE',
1075
+ providerIncomplete: true,
1076
+ finishReason: stopReason,
1077
+ partialContent: choice?.message?.content || '',
1078
+ partialToolCalls: toolCalls,
1079
+ model: response.model || useModel,
1080
+ responseId: response.id || null,
1081
+ rawUsage: response.usage || null,
1082
+ },
1083
+ );
1084
+ throw err;
1085
+ }
1086
+ writeCompatCacheTrace({
1087
+ provider: this.name,
1088
+ model: useModel,
1089
+ opts,
1090
+ params,
1091
+ rawTools: tools || [],
1092
+ response,
1093
+ cacheRoutingKey,
1094
+ cacheRouting,
1095
+ });
1096
+ if (response.usage) {
1097
+ const inputTokens = Number(response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0);
1098
+ const cachedTokens = extractCompatCachedTokens(response.usage);
1099
+ traceBridgeUsage({
1100
+ sessionId: opts.sessionId || opts.session?.id || null,
1101
+ iteration: Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null,
1102
+ inputTokens,
1103
+ outputTokens: Number(response.usage.completion_tokens ?? response.usage.output_tokens ?? 0),
1104
+ cachedTokens,
1105
+ cacheWriteTokens: 0,
1106
+ promptTokens: inputTokens,
1107
+ model: response.model || useModel,
1108
+ modelDisplay: response.model || useModel,
1109
+ responseId: response.id || null,
1110
+ rawUsage: response.usage,
1111
+ provider: this.name,
1112
+ });
1113
+ }
1114
+ // Capture provider reasoning_content so loop.mjs can attach it to the
1115
+ // assistant message and echo it back next turn for providers that
1116
+ // require or benefit from that official multi-turn shape.
1117
+ const capturesReasoningContent = this.name === 'deepseek' || this.name === 'xai';
1118
+ const reasoningContent = (capturesReasoningContent && typeof choice?.message?.reasoning_content === 'string')
1119
+ ? choice.message.reasoning_content
1120
+ : null;
1121
+ return {
1122
+ content: choice?.message?.content || '',
1123
+ model: response.model,
1124
+ toolCalls,
1125
+ stopReason,
1126
+ ...(reasoningContent ? { reasoningContent } : {}),
1127
+ usage: response.usage ? (() => {
1128
+ const input = response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0;
1129
+ const cached = extractCompatCachedTokens(response.usage);
1130
+ // xAI Grok returns the actual billed amount in `cost_in_usd_ticks`
1131
+ // (1 tick = $1e-10, per docs.x.ai). Surface it as costUsd so the
1132
+ // session manager skips the catalog-rate fallback and records the
1133
+ // provider-billed value verbatim.
1134
+ const ticks = response.usage.cost_in_usd_ticks;
1135
+ const costUsd = typeof ticks === 'number' && ticks >= 0
1136
+ ? Number((ticks * 1e-10).toFixed(8))
1137
+ : undefined;
1138
+ return {
1139
+ inputTokens: input,
1140
+ outputTokens: response.usage.completion_tokens ?? response.usage.output_tokens ?? 0,
1141
+ cachedTokens: cached,
1142
+ // Chat Completions prompt_tokens is already the total prompt
1143
+ // the model ingested (cached is a subset) — alias directly.
1144
+ promptTokens: input,
1145
+ raw: { ...response.usage },
1146
+ ...(costUsd != null ? { costUsd } : {}),
1147
+ };
1148
+ })() : undefined,
1149
+ };
1150
+ }
1151
+ async _doSendXaiResponses(messages, useModel, tools, opts) {
1152
+ const signal = opts.signal || null;
1153
+ if (signal?.aborted) {
1154
+ const reason = signal.reason;
1155
+ throw reason instanceof Error ? reason : new Error('xAI Responses request aborted by session close');
1156
+ }
1157
+ const chatMessagesForTrace = toOpenAIMessages(messages, this.name);
1158
+ const cacheRouting = xaiResponsesCacheRouting(opts, { messages: chatMessagesForTrace }, tools || [], useModel);
1159
+ const { input, previousResponseId, startIndex } = toXaiResponsesInput(messages, opts.providerState);
1160
+ const params = {
1161
+ model: useModel,
1162
+ input,
1163
+ store: true,
1164
+ prompt_cache_key: cacheRouting.key,
1165
+ };
1166
+ if (previousResponseId) params.previous_response_id = previousResponseId;
1167
+ if (tools?.length) params.tools = toResponsesTools(tools);
1168
+ // Non-streaming transport: there are no deltas to report, so without
1169
+ // an explicit stage the session sits on the loop's per-iteration
1170
+ // 'connecting' reset for the whole generation (bridge list shows a
1171
+ // working session as stuck). Report 'requesting' for the in-flight
1172
+ // window and fire one delta on arrival to feed the stall watchdog.
1173
+ try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
1174
+ const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
1175
+ ?? opts.effort
1176
+ ?? this.config?.reasoningEffort
1177
+ ?? process.env.MIXDOG_XAI_REASONING_EFFORT);
1178
+ if (reasoningEffort) params.reasoning = { effort: reasoningEffort };
1179
+ let response;
1180
+ let cacheLane = null;
1181
+ const scheduled = await withXaiResponsesCacheLane({
1182
+ opts,
1183
+ config: this.config,
1184
+ cacheRouting,
1185
+ model: useModel,
1186
+ transport: 'http',
1187
+ previousResponseId,
1188
+ inputCount: Array.isArray(input) ? input.length : 0,
1189
+ signal,
1190
+ }, async (laneMeta) => {
1191
+ cacheLane = laneMeta;
1192
+ const totalSignal = createTimeoutSignal(signal, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, 'xai responses total');
1193
+ try {
1194
+ return await withRetry(
1195
+ ({ signal: attemptSignal }) => this.client.responses.create(params, { signal: attemptSignal }),
1196
+ {
1197
+ signal: totalSignal.signal,
1198
+ perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
1199
+ perAttemptLabel: 'xai responses first byte',
1200
+ onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
1201
+ const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
1202
+ process.stderr.write(`[xai:responses] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
1203
+ },
1204
+ },
1205
+ );
1206
+ } finally {
1207
+ totalSignal.cleanup();
1208
+ }
1209
+ });
1210
+ response = scheduled.value;
1211
+ cacheLane = cacheLane || scheduled.laneMeta;
1212
+ try { opts.onStreamDelta?.(); } catch { /* heartbeat best-effort */ }
1213
+ const toolCalls = parseResponsesToolCalls(response);
1214
+ writeXaiResponsesCacheTrace({
1215
+ model: useModel,
1216
+ opts,
1217
+ params,
1218
+ rawTools: tools || [],
1219
+ response,
1220
+ cacheRouting,
1221
+ previousResponseId,
1222
+ inputStartIndex: startIndex,
1223
+ transport: 'http',
1224
+ cacheLane,
1225
+ });
1226
+ traceXaiResponsesCacheContext({
1227
+ model: useModel,
1228
+ opts,
1229
+ params,
1230
+ rawTools: tools || [],
1231
+ response,
1232
+ cacheRouting,
1233
+ previousResponseId,
1234
+ inputStartIndex: startIndex,
1235
+ transport: 'http',
1236
+ cacheLane,
1237
+ });
1238
+ if (response.usage) {
1239
+ const inputTokens = Number(response.usage.input_tokens ?? response.usage.prompt_tokens ?? 0);
1240
+ const cachedTokens = extractCompatCachedTokens(response.usage);
1241
+ traceBridgeUsage({
1242
+ sessionId: opts.sessionId || opts.session?.id || null,
1243
+ iteration: Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null,
1244
+ inputTokens,
1245
+ outputTokens: Number(response.usage.output_tokens ?? response.usage.completion_tokens ?? 0),
1246
+ cachedTokens,
1247
+ cacheWriteTokens: 0,
1248
+ promptTokens: inputTokens,
1249
+ model: response.model || useModel,
1250
+ modelDisplay: response.model || useModel,
1251
+ responseId: response.id || null,
1252
+ rawUsage: response.usage,
1253
+ provider: 'xai',
1254
+ });
1255
+ }
1256
+ return {
1257
+ content: responseOutputText(response),
1258
+ model: response.model || useModel,
1259
+ toolCalls,
1260
+ providerState: {
1261
+ ...(opts.providerState || {}),
1262
+ xaiResponses: {
1263
+ previousResponseId: response.id,
1264
+ seenMessageCount: Array.isArray(messages) ? messages.length : 0,
1265
+ model: response.model || useModel,
1266
+ updatedAt: Date.now(),
1267
+ },
1268
+ },
1269
+ usage: response.usage ? (() => {
1270
+ const inputTokens = response.usage.input_tokens ?? response.usage.prompt_tokens ?? 0;
1271
+ const ticks = response.usage.cost_in_usd_ticks;
1272
+ const costUsd = typeof ticks === 'number' && ticks >= 0
1273
+ ? Number((ticks * 1e-10).toFixed(8))
1274
+ : undefined;
1275
+ return {
1276
+ inputTokens,
1277
+ outputTokens: response.usage.output_tokens ?? response.usage.completion_tokens ?? 0,
1278
+ cachedTokens: extractCompatCachedTokens(response.usage),
1279
+ promptTokens: inputTokens,
1280
+ raw: { ...response.usage },
1281
+ ...(costUsd != null ? { costUsd } : {}),
1282
+ };
1283
+ })() : undefined,
1284
+ };
1285
+ }
1286
+ async _doSendXaiResponsesWebSocket(messages, useModel, tools, opts) {
1287
+ const signal = opts.signal || null;
1288
+ if (signal?.aborted) {
1289
+ const reason = signal.reason;
1290
+ throw reason instanceof Error ? reason : new Error('xAI Responses WebSocket request aborted by session close');
1291
+ }
1292
+ const apiKey = this.config?.apiKey || process.env.XAI_API_KEY;
1293
+ if (!apiKey) throw new Error('xAI API key not configured');
1294
+ const chatMessagesForTrace = toOpenAIMessages(messages, this.name);
1295
+ const cacheRouting = xaiResponsesCacheRouting(opts, { messages: chatMessagesForTrace }, tools || [], useModel);
1296
+ const { input, previousResponseId, startIndex } = toXaiResponsesInput(messages, opts.providerState, { includeSystem: false });
1297
+ const params = {
1298
+ model: useModel,
1299
+ input,
1300
+ // xAI's WebSocket continuation is documented for store=false, but
1301
+ // the public endpoint currently returns previous_response_not_found
1302
+ // in our live probes unless the chain is stored.
1303
+ store: true,
1304
+ prompt_cache_key: cacheRouting.key,
1305
+ };
1306
+ const instructions = xaiSystemInstructions(messages);
1307
+ if (previousResponseId) params.previous_response_id = previousResponseId;
1308
+ // xAI rejects instructions together with previous_response_id; the
1309
+ // first response already anchors instructions for the continuation.
1310
+ else if (instructions) params.instructions = instructions;
1311
+ if (tools?.length) params.tools = toResponsesTools(tools);
1312
+ const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
1313
+ ?? opts.effort
1314
+ ?? this.config?.reasoningEffort
1315
+ ?? process.env.MIXDOG_XAI_REASONING_EFFORT);
1316
+ if (reasoningEffort) params.reasoning = { effort: reasoningEffort };
1317
+ const warmupBody = useXaiResponsesWebSocketWarmup(opts, this.config, {
1318
+ previousResponseId,
1319
+ instructions,
1320
+ rawTools: tools || [],
1321
+ })
1322
+ ? { ...params, generate: false, input: [] }
1323
+ : null;
1324
+ const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
1325
+ let cacheLane = null;
1326
+ const scheduled = await withXaiResponsesCacheLane({
1327
+ opts,
1328
+ config: this.config,
1329
+ cacheRouting,
1330
+ model: useModel,
1331
+ transport: 'websocket',
1332
+ previousResponseId,
1333
+ inputCount: Array.isArray(input) ? input.length : 0,
1334
+ signal,
1335
+ }, async (laneMeta) => {
1336
+ cacheLane = laneMeta;
1337
+ return await sendViaWebSocket({
1338
+ auth: { type: 'xai', apiKey },
1339
+ body: params,
1340
+ sendOpts: opts,
1341
+ onStreamDelta: typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null,
1342
+ onToolCall: typeof opts.onToolCall === 'function' ? opts.onToolCall : null,
1343
+ onStageChange: typeof opts.onStageChange === 'function' ? opts.onStageChange : null,
1344
+ externalSignal: signal,
1345
+ poolKey: opts.sessionId || opts.session?.id || null,
1346
+ cacheKey: cacheRouting.key,
1347
+ iteration,
1348
+ useModel,
1349
+ displayModel: (id) => id,
1350
+ includeResponseId: true,
1351
+ traceProvider: 'xai',
1352
+ logSuppressedReasoningDeltas: false,
1353
+ warmupBody,
1354
+ });
1355
+ });
1356
+ const result = scheduled.value;
1357
+ cacheLane = cacheLane || scheduled.laneMeta;
1358
+ const responseId = result.responseId || previousResponseId || null;
1359
+ const rawUsage = result.usage?.raw || result.usage || null;
1360
+ const traceParams = result.__warmup?.requestBody || params;
1361
+ writeXaiResponsesCacheTrace({
1362
+ model: useModel,
1363
+ opts,
1364
+ params: traceParams,
1365
+ rawTools: tools || [],
1366
+ response: {
1367
+ id: responseId,
1368
+ model: result.model || useModel,
1369
+ output: [],
1370
+ usage: rawUsage,
1371
+ },
1372
+ cacheRouting,
1373
+ previousResponseId,
1374
+ inputStartIndex: startIndex,
1375
+ transport: 'websocket',
1376
+ cacheLane,
1377
+ });
1378
+ traceXaiResponsesCacheContext({
1379
+ model: useModel,
1380
+ opts,
1381
+ params: traceParams,
1382
+ rawTools: tools || [],
1383
+ response: {
1384
+ id: responseId,
1385
+ model: result.model || useModel,
1386
+ output: [],
1387
+ usage: rawUsage,
1388
+ },
1389
+ cacheRouting,
1390
+ previousResponseId,
1391
+ inputStartIndex: startIndex,
1392
+ transport: 'websocket',
1393
+ cacheLane,
1394
+ });
1395
+ const ticks = rawUsage?.cost_in_usd_ticks;
1396
+ const costUsd = typeof ticks === 'number' && ticks >= 0
1397
+ ? Number((ticks * 1e-10).toFixed(8))
1398
+ : undefined;
1399
+ return {
1400
+ content: result.content || '',
1401
+ model: result.model || useModel,
1402
+ toolCalls: result.toolCalls,
1403
+ providerState: {
1404
+ ...(opts.providerState || {}),
1405
+ xaiResponses: {
1406
+ previousResponseId: responseId,
1407
+ seenMessageCount: Array.isArray(messages) ? messages.length : 0,
1408
+ model: result.model || useModel,
1409
+ updatedAt: Date.now(),
1410
+ transport: 'websocket',
1411
+ },
1412
+ },
1413
+ usage: result.usage ? {
1414
+ ...result.usage,
1415
+ ...(costUsd != null ? { costUsd } : {}),
1416
+ } : undefined,
1417
+ };
1418
+ }
1419
+ async _fetchModelItems() {
1420
+ const timeout = createTimeoutSignal(null, MODEL_LIST_TIMEOUT_MS, `${this.name} model list`);
1421
+ try {
1422
+ const res = await fetch(`${String(this.baseURL || '').replace(/\/+$/, '')}/models`, {
1423
+ method: 'GET',
1424
+ headers: {
1425
+ Authorization: `Bearer ${this.apiKey || 'no-key'}`,
1426
+ ...(this.defaultHeaders || {}),
1427
+ },
1428
+ signal: timeout.signal,
1429
+ });
1430
+ if (!res.ok) throw new Error(`${this.name} models ${res.status}`);
1431
+ const data = await res.json();
1432
+ if (Array.isArray(data?.data)) return data.data;
1433
+ if (Array.isArray(data)) return data;
1434
+ return [];
1435
+ } finally {
1436
+ timeout.cleanup();
1437
+ }
1438
+ }
1439
+ async listModels() {
1440
+ try {
1441
+ const list = await this._fetchModelItems();
1442
+ const models = [];
1443
+ for (const m of list) {
1444
+ models.push({
1445
+ id: m?.id,
1446
+ name: m?.id,
1447
+ provider: this.name,
1448
+ contextWindow: 0,
1449
+ created: typeof m?.created === 'number' ? m.created : null,
1450
+ });
1451
+ }
1452
+ return models.filter(m => m.id);
1453
+ }
1454
+ catch {
1455
+ return [];
1456
+ }
1457
+ }
1458
+ async isAvailable() {
1459
+ try {
1460
+ await this._fetchModelItems();
1461
+ return true;
1462
+ }
1463
+ catch {
1464
+ return false;
1465
+ }
1466
+ }
1467
+ }