mixdog 0.9.2 → 0.9.4

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 (417) hide show
  1. package/package.json +8 -3
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/review-tasks.json +20 -0
  7. package/scripts/bench/round-codex.json +114 -0
  8. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  9. package/scripts/bench/round-mixdog-lead.json +269 -0
  10. package/scripts/bench/round-mixdog.json +126 -0
  11. package/scripts/bench/round-r10-bigsample.json +679 -0
  12. package/scripts/bench/round-r11-codexalign.json +257 -0
  13. package/scripts/bench/round-r4-codex.json +114 -0
  14. package/scripts/bench/round-r4-mixed.json +225 -0
  15. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  16. package/scripts/bench/round-r6-codex.json +114 -0
  17. package/scripts/bench/round-r6-solo.json +257 -0
  18. package/scripts/bench/round-r7-full.json +254 -0
  19. package/scripts/bench/round-r8-fulldefault.json +255 -0
  20. package/scripts/bench-run.mjs +215 -29
  21. package/scripts/build-tui.mjs +13 -1
  22. package/scripts/explore-bench.mjs +124 -0
  23. package/scripts/freevar-smoke.mjs +95 -0
  24. package/scripts/hook-bus-test.mjs +191 -0
  25. package/scripts/internal-comms-bench.mjs +1 -0
  26. package/scripts/internal-comms-smoke.mjs +10 -9
  27. package/scripts/mouse-probe.mjs +45 -0
  28. package/scripts/output-style-bench.mjs +13 -6
  29. package/scripts/output-style-smoke.mjs +4 -4
  30. package/scripts/path-suffix-test.mjs +57 -0
  31. package/scripts/provider-toolcall-test.mjs +7 -3
  32. package/scripts/recall-bench.mjs +207 -0
  33. package/scripts/recall-usecase-cases.json +18 -0
  34. package/scripts/recall-usecase-probe.json +6 -0
  35. package/scripts/session-bench.mjs +152 -6
  36. package/scripts/tool-smoke.mjs +30 -67
  37. package/scripts/tui-render-smoke.mjs +90 -0
  38. package/scripts/webhook-smoke.mjs +208 -0
  39. package/src/agents/debugger/AGENT.md +5 -2
  40. package/src/agents/heavy-worker/AGENT.md +21 -11
  41. package/src/agents/maintainer/AGENT.md +4 -0
  42. package/src/agents/reviewer/AGENT.md +3 -2
  43. package/src/agents/worker/AGENT.md +21 -11
  44. package/src/lib/rules-builder.cjs +4 -0
  45. package/src/mixdog-session-runtime.mjs +933 -3731
  46. package/src/output-styles/default.md +34 -9
  47. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  48. package/src/output-styles/minimal.md +4 -1
  49. package/src/output-styles/simple.md +22 -7
  50. package/src/repl.mjs +5 -5
  51. package/src/rules/agent/00-common.md +2 -0
  52. package/src/rules/agent/30-explorer.md +8 -11
  53. package/src/rules/lead/lead-brief.md +12 -0
  54. package/src/rules/lead/lead-tool.md +2 -9
  55. package/src/rules/shared/01-tool.md +11 -5
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  60. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  61. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  62. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  63. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
  66. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
  70. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  71. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
  72. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  73. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  74. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  75. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  76. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  77. package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
  78. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  80. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  81. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  82. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  83. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
  89. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  90. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
  92. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
  93. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  94. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  95. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  96. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  97. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  99. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  100. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  101. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  102. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  103. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  107. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  108. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  109. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
  110. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  111. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  112. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  113. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  114. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
  115. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  116. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  117. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  118. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  120. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  121. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  122. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  123. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  124. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  125. package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
  126. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  127. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
  128. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  129. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
  130. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  131. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  132. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  133. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  137. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  138. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  141. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
  142. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  145. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  146. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  148. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  149. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
  152. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
  155. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
  156. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  157. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  158. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  159. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  161. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  163. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  164. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  165. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  166. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
  167. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  168. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  169. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  170. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  171. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  172. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  173. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  174. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  175. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  176. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  177. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  178. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  179. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  180. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  181. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  182. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  183. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  184. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
  185. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
  186. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  187. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  188. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  189. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  190. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  191. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  192. package/src/runtime/channels/backends/discord.mjs +12 -292
  193. package/src/runtime/channels/index.mjs +241 -894
  194. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  195. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  196. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  197. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  198. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  199. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  200. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  201. package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
  202. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  203. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  204. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  205. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  206. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  207. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  208. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  209. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  210. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  211. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  212. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  213. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  214. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  215. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  216. package/src/runtime/channels/lib/webhook.mjs +36 -570
  217. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  218. package/src/runtime/channels/tool-defs.mjs +11 -130
  219. package/src/runtime/memory/index.mjs +258 -2050
  220. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  221. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  222. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  223. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  224. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  225. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  226. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  227. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  228. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  229. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  230. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  231. package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
  232. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  233. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  234. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  235. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  236. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  237. package/src/runtime/memory/lib/memory.mjs +20 -0
  238. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  239. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  240. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  241. package/src/runtime/memory/lib/recall-format.mjs +238 -0
  242. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  243. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  244. package/src/runtime/memory/tool-defs.mjs +6 -14
  245. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  246. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  247. package/src/runtime/search/lib/web-tools.mjs +24 -602
  248. package/src/runtime/shared/abort-controller.mjs +1 -1
  249. package/src/runtime/shared/atomic-file.mjs +26 -1
  250. package/src/runtime/shared/background-tasks.mjs +2 -3
  251. package/src/runtime/shared/buffered-appender.mjs +149 -0
  252. package/src/runtime/shared/launcher-control.mjs +2 -2
  253. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  254. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  255. package/src/runtime/shared/tool-primitives.mjs +308 -0
  256. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  257. package/src/runtime/shared/tool-surface.mjs +80 -898
  258. package/src/runtime/shared/transcript-writer.mjs +52 -2
  259. package/src/runtime/shared/update-checker.mjs +7 -4
  260. package/src/session-runtime/config-helpers.mjs +291 -0
  261. package/src/session-runtime/config-lifecycle.mjs +232 -0
  262. package/src/session-runtime/cwd-plugins.mjs +226 -0
  263. package/src/session-runtime/effort.mjs +128 -0
  264. package/src/session-runtime/fs-utils.mjs +10 -0
  265. package/src/session-runtime/mcp-glue.mjs +177 -0
  266. package/src/session-runtime/model-capabilities.mjs +130 -0
  267. package/src/session-runtime/model-recency.mjs +111 -0
  268. package/src/session-runtime/native-search.mjs +247 -0
  269. package/src/session-runtime/output-styles.mjs +126 -0
  270. package/src/session-runtime/plugin-mcp.mjs +114 -0
  271. package/src/session-runtime/prewarm.mjs +142 -0
  272. package/src/session-runtime/provider-models.mjs +278 -0
  273. package/src/session-runtime/provider-usage.mjs +120 -0
  274. package/src/session-runtime/quick-model-rows.mjs +170 -0
  275. package/src/session-runtime/quick-search-models.mjs +46 -0
  276. package/src/session-runtime/session-hooks.mjs +93 -0
  277. package/src/session-runtime/session-text.mjs +100 -0
  278. package/src/session-runtime/settings-api.mjs +319 -0
  279. package/src/session-runtime/statusline-route.mjs +35 -0
  280. package/src/session-runtime/tool-catalog.mjs +720 -0
  281. package/src/session-runtime/tool-defs.mjs +84 -0
  282. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  283. package/src/session-runtime/workflow.mjs +358 -0
  284. package/src/standalone/agent-tool/helpers.mjs +237 -0
  285. package/src/standalone/agent-tool/notify.mjs +107 -0
  286. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  287. package/src/standalone/agent-tool/render.mjs +152 -0
  288. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  289. package/src/standalone/agent-tool.mjs +155 -677
  290. package/src/standalone/channel-worker.mjs +7 -9
  291. package/src/standalone/explore-tool.mjs +40 -12
  292. package/src/standalone/hook-bus/config.mjs +207 -0
  293. package/src/standalone/hook-bus/constants.mjs +90 -0
  294. package/src/standalone/hook-bus/handlers.mjs +481 -0
  295. package/src/standalone/hook-bus/payload.mjs +31 -0
  296. package/src/standalone/hook-bus/rules.mjs +77 -0
  297. package/src/standalone/hook-bus.mjs +110 -746
  298. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  299. package/src/standalone/opencode-go-login.mjs +125 -0
  300. package/src/standalone/provider-admin.mjs +15 -19
  301. package/src/standalone/usage-dashboard.mjs +3 -1
  302. package/src/tui/App.jsx +1163 -7571
  303. package/src/tui/app/app-format.mjs +206 -0
  304. package/src/tui/app/channel-pickers.mjs +510 -0
  305. package/src/tui/app/clipboard.mjs +67 -0
  306. package/src/tui/app/core-memory-picker.mjs +210 -0
  307. package/src/tui/app/extension-pickers.mjs +506 -0
  308. package/src/tui/app/input-parsers.mjs +193 -0
  309. package/src/tui/app/maintenance-pickers.mjs +324 -0
  310. package/src/tui/app/model-options.mjs +330 -0
  311. package/src/tui/app/model-picker.mjs +365 -0
  312. package/src/tui/app/onboarding-steps.mjs +400 -0
  313. package/src/tui/app/project-picker.mjs +247 -0
  314. package/src/tui/app/provider-setup-picker.mjs +580 -0
  315. package/src/tui/app/resume-picker.mjs +55 -0
  316. package/src/tui/app/route-pickers.mjs +419 -0
  317. package/src/tui/app/settings-picker.mjs +490 -0
  318. package/src/tui/app/slash-commands.mjs +101 -0
  319. package/src/tui/app/slash-dispatch.mjs +427 -0
  320. package/src/tui/app/text-layout.mjs +46 -0
  321. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  322. package/src/tui/app/transcript-window.mjs +671 -0
  323. package/src/tui/app/use-mouse-input.mjs +460 -0
  324. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  325. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  326. package/src/tui/app/use-transcript-window.mjs +589 -0
  327. package/src/tui/components/ConfirmBar.jsx +1 -1
  328. package/src/tui/components/Picker.jsx +32 -4
  329. package/src/tui/components/PromptInput.jsx +259 -80
  330. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  331. package/src/tui/components/StatusLine.jsx +63 -12
  332. package/src/tui/components/TextEntryPanel.jsx +11 -0
  333. package/src/tui/components/ToolExecution.jsx +56 -588
  334. package/src/tui/components/TranscriptItem.jsx +105 -0
  335. package/src/tui/components/UsagePanel.jsx +18 -4
  336. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  337. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  338. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  339. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  340. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  341. package/src/tui/components/tool-output-format.mjs +2 -2
  342. package/src/tui/display-width.mjs +20 -3
  343. package/src/tui/dist/index.mjs +18034 -17188
  344. package/src/tui/engine/agent-envelope.mjs +296 -0
  345. package/src/tui/engine/agent-job-feed.mjs +133 -0
  346. package/src/tui/engine/boot-profile.mjs +21 -0
  347. package/src/tui/engine/labels.mjs +67 -0
  348. package/src/tui/engine/notice-text.mjs +112 -0
  349. package/src/tui/engine/notification-plan.mjs +76 -0
  350. package/src/tui/engine/queue-helpers.mjs +161 -0
  351. package/src/tui/engine/render-timing.mjs +17 -0
  352. package/src/tui/engine/session-stats.mjs +46 -0
  353. package/src/tui/engine/tool-approval.mjs +94 -0
  354. package/src/tui/engine/tool-call-fields.mjs +23 -0
  355. package/src/tui/engine/tool-card-results.mjs +234 -0
  356. package/src/tui/engine/tool-result-status.mjs +135 -0
  357. package/src/tui/engine/tool-result-text.mjs +126 -0
  358. package/src/tui/engine.mjs +405 -1385
  359. package/src/tui/figures.mjs +5 -0
  360. package/src/tui/index.jsx +105 -0
  361. package/src/tui/input-editing.mjs +60 -10
  362. package/src/tui/keyboard-protocol.mjs +2 -2
  363. package/src/tui/lib/voice-recorder.mjs +35 -19
  364. package/src/tui/markdown/format-token.mjs +11 -9
  365. package/src/tui/paste-attachments.mjs +38 -0
  366. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  367. package/src/tui/theme.mjs +6 -0
  368. package/src/tui/themes/base.mjs +2 -2
  369. package/src/tui/themes/kanagawa.mjs +4 -4
  370. package/src/tui/themes/teal.mjs +4 -5
  371. package/src/tui/themes/utils.mjs +1 -1
  372. package/src/ui/statusline-agents.mjs +213 -0
  373. package/src/ui/statusline-format.mjs +146 -0
  374. package/src/ui/statusline-segments.mjs +148 -0
  375. package/src/ui/statusline.mjs +77 -462
  376. package/src/ui/tool-card.mjs +0 -1
  377. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  378. package/src/workflows/default/WORKFLOW.md +16 -9
  379. package/src/workflows/sequential/WORKFLOW.md +16 -11
  380. package/src/workflows/solo/WORKFLOW.md +5 -1
  381. package/vendor/ink/build/display-width.js +19 -3
  382. package/vendor/ink/build/ink.js +103 -6
  383. package/vendor/ink/build/log-update.js +17 -3
  384. package/vendor/ink/build/wrap-text.js +125 -0
  385. package/scripts/_test-folder-dialog.mjs +0 -30
  386. package/scripts/fix-brief-fn.mjs +0 -35
  387. package/scripts/fix-format-tool-surface.mjs +0 -24
  388. package/scripts/fix-tool-exec-visible.mjs +0 -42
  389. package/scripts/patch-agent-brief.mjs +0 -48
  390. package/scripts/patch-app.mjs +0 -21
  391. package/scripts/patch-app2.mjs +0 -18
  392. package/scripts/patch-dist-brief.mjs +0 -96
  393. package/scripts/patch-tool-exec.mjs +0 -70
  394. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  395. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  396. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  397. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  398. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  399. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  400. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  401. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  402. package/src/standalone/agent-task-status.test.mjs +0 -76
  403. package/src/tui/components/tool-output-format.test.mjs +0 -399
  404. package/src/tui/display-width.test.mjs +0 -35
  405. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  406. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  407. package/src/tui/markdown/format-token.test.mjs +0 -354
  408. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  409. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  410. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  411. package/src/tui/prompt-history-store.test.mjs +0 -52
  412. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  413. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  414. package/src/ui/markdown.test.mjs +0 -70
  415. package/src/ui/statusline-context-label.test.mjs +0 -15
  416. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  417. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -1,4 +1,4 @@
1
- import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai';
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
2
  import { loadConfig } from '../config.mjs';
3
3
  import { makeModelCache } from './model-cache.mjs';
4
4
  import { withRetry } from './retry-classifier.mjs';
@@ -14,9 +14,43 @@ import {
14
14
  createPassthroughSignal,
15
15
  } from '../stall-policy.mjs';
16
16
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
17
- import { traceHash, stableTraceStringify, summarizeTraceTools, traceTextShape } from './trace-utils.mjs';
18
- import { normalizeContentForGeminiParts, splitToolContentForGemini } from './media-normalization.mjs';
19
- import { scanLeakedToolCalls } from './anthropic-leaked-toolcall.mjs';
17
+ import {
18
+ GEMINI_FIRST_BYTE_TIMEOUT_MS,
19
+ geminiTimeoutError,
20
+ createGeminiTextLeakGuard,
21
+ consumeGeminiRestStreamResponse,
22
+ consumeGeminiSdkStream,
23
+ } from './gemini-stream.mjs';
24
+ import {
25
+ toGeminiTools,
26
+ toGeminiNativeTools,
27
+ toGeminiToolConfig,
28
+ toGeminiContents,
29
+ parseToolCalls,
30
+ emitGeminiToolCalls,
31
+ collectGeminiGroundingSources,
32
+ } from './gemini-schema.mjs';
33
+ import {
34
+ _estimateGeminiCacheTokens,
35
+ _geminiCacheMinTokens,
36
+ _geminiCachePrefixCount,
37
+ _geminiCachePrefixContents,
38
+ _geminiCachePrefixHash,
39
+ _geminiGlobalCacheKey,
40
+ _getGeminiGlobalCache,
41
+ _setGeminiGlobalCache,
42
+ _geminiGlobalCacheNameIsLive,
43
+ _attachGeminiCacheState,
44
+ _resolveGeminiCacheUsage,
45
+ writeGeminiCacheTrace,
46
+ geminiGlobalCacheCreates,
47
+ GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS,
48
+ } from './gemini-cache.mjs';
49
+
50
+ // Legacy import path: tests + gemini-stream import these from gemini.mjs.
51
+ // Re-export the extracted symbols so existing importers resolve unchanged.
52
+ export { createGeminiTextLeakGuard };
53
+ export { parseToolCalls, emitGeminiToolCalls, collectGeminiGroundingSources };
20
54
 
21
55
  const MODELS = [
22
56
  { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
@@ -33,7 +67,7 @@ const DEFAULT_MODEL = MODELS[0].id;
33
67
  // VERSION-based (parse gemini-X.Y) rather than release-date based.
34
68
  const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
35
69
  // Bump when the on-disk cache shape changes so stale-shape entries are
36
- // discarded instead of misread (mirrors openai-oauth's schema-version gate).
70
+ // discarded instead of misread.
37
71
  const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 1;
38
72
 
39
73
  // De-dupes concurrent force-refreshes so they share one HTTP round-trip,
@@ -94,1014 +128,9 @@ export async function ensureLatestGeminiModel(provider) {
94
128
  throw new Error('[gemini] model catalog unavailable after warmup — cannot resolve default model');
95
129
  }
96
130
 
97
- const GEMINI_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
98
- 'MIXDOG_GEMINI_FIRST_BYTE_TIMEOUT_MS',
99
- PROVIDER_FIRST_BYTE_TIMEOUT_MS,
100
- { minMs: 30_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
101
- );
102
-
103
- function summarizeTracePart(part) {
104
- if (!part || typeof part !== 'object') return { type: typeof part };
105
- if ('text' in part) {
106
- return { type: 'text', ...traceTextShape(part.text) };
107
- }
108
- if (part.functionCall) {
109
- return {
110
- type: 'functionCall',
111
- name: part.functionCall.name || null,
112
- argsHash: traceHash(stableTraceStringify(part.functionCall.args || {})),
113
- };
114
- }
115
- if (part.functionResponse) {
116
- const response = part.functionResponse.response || {};
117
- const responseShape = stableTraceStringify(response);
118
- return {
119
- type: 'functionResponse',
120
- name: part.functionResponse.name || null,
121
- responseChars: responseShape.length,
122
- responseHash: traceHash(responseShape),
123
- };
124
- }
125
- return { type: Object.keys(part).sort().join('|') || 'unknown' };
126
- }
127
-
128
- function summarizeTraceContents(contents) {
129
- const summaries = (contents || []).map((content, index) => ({
130
- index,
131
- role: content?.role || null,
132
- parts: Array.isArray(content?.parts) ? content.parts.map(summarizeTracePart) : [],
133
- }));
134
- if (summaries.length <= 12) return summaries;
135
- return [
136
- ...summaries.slice(0, 8),
137
- { omittedTurns: summaries.length - 12 },
138
- ...summaries.slice(-4),
139
- ];
140
- }
141
-
142
- // Gemini cachedContents API rejects prefixes below the model-specific minimum
143
- // (Gemini 2.0/2.5 = 2048 tokens, Gemini 3.x = 4096 tokens) with HTTP 400
144
- // "Cached content is too small". Estimating chars/4 ≈ tokens lets us skip the
145
- // roundtrip when the prefix cannot satisfy the invariant. Estimate undercount
146
- // is harmless (we attempt and get the same 400 we would have skipped); overcount
147
- // is harmless (we skip, fall back to implicit cache).
148
- function _estimateGeminiCacheTokens(systemInstruction, geminiTools, contents) {
149
- let chars = 0;
150
- if (typeof systemInstruction === 'string') chars += systemInstruction.length;
151
- if (Array.isArray(geminiTools) && geminiTools.length) {
152
- try { chars += JSON.stringify(geminiTools).length; } catch {}
153
- }
154
- if (Array.isArray(contents) && contents.length > 1) {
155
- for (let i = 0; i < contents.length - 1; i++) {
156
- try { chars += JSON.stringify(contents[i]?.parts ?? '').length; } catch {}
157
- }
158
- }
159
- return Math.ceil(chars / 4);
160
- }
161
-
162
- function _geminiCacheMinTokens(model) {
163
- return /^gemini-3/i.test(String(model || '')) ? 4096 : 2048;
164
- }
165
-
166
- function _geminiCachePrefixCount(contents) {
167
- return Array.isArray(contents) && contents.length > 1 ? contents.length - 1 : 0;
168
- }
169
-
170
- function _geminiCachePrefixContents(contents, prefixCount) {
171
- if (!Array.isArray(contents) || prefixCount <= 0) return [];
172
- return contents.slice(0, prefixCount).map(c => {
173
- const r = c?.role;
174
- const safeRole = (r === 'model' || r === 'user') ? r : 'user';
175
- return {
176
- role: safeRole,
177
- parts: Array.isArray(c?.parts) ? c.parts : [],
178
- };
179
- });
180
- }
181
-
182
- function _geminiCachePrefixHash({ model, systemInstruction, geminiTools, contents, prefixCount }) {
183
- return traceHash(stableTraceStringify({
184
- model: model || null,
185
- systemInstruction: systemInstruction || '',
186
- tools: geminiTools || [],
187
- contents: _geminiCachePrefixContents(contents, prefixCount),
188
- }));
189
- }
190
-
191
- const GEMINI_GLOBAL_CACHE_MIN_LIVE_MS = 6 * 60 * 1000;
192
- const GEMINI_GLOBAL_CACHE_MAX_ENTRIES = 128;
193
- // Grace window before deleting a superseded cachedContents name (see the
194
- // cross-session race note at the L1341-1372 call site). Long enough that a
195
- // concurrent session still mid-flight on the old name has time to finish.
196
- const GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS = 2 * 60 * 1000;
197
- const geminiGlobalCaches = new Map();
198
- const geminiGlobalCacheCreates = new Map();
199
-
200
- function _geminiGlobalCacheKey({ apiKey, model, cachePrefixHash, cachePrefixContentCount }) {
201
- return traceHash(stableTraceStringify({
202
- apiKeyHash: traceHash(apiKey || ''),
203
- model: model || null,
204
- cachePrefixHash,
205
- cachePrefixContentCount,
206
- }));
207
- }
208
-
209
- function _pruneGeminiGlobalCaches(now = Date.now()) {
210
- for (const [key, entry] of geminiGlobalCaches) {
211
- if (!entry?.cacheName || Number(entry.cacheExpiresAt || 0) <= now) {
212
- geminiGlobalCaches.delete(key);
213
- }
214
- }
215
- while (geminiGlobalCaches.size > GEMINI_GLOBAL_CACHE_MAX_ENTRIES) {
216
- const oldest = geminiGlobalCaches.keys().next().value;
217
- if (oldest === undefined) break;
218
- geminiGlobalCaches.delete(oldest);
219
- }
220
- }
221
-
222
- function _getGeminiGlobalCache(key, now = Date.now()) {
223
- _pruneGeminiGlobalCaches(now);
224
- const entry = geminiGlobalCaches.get(key);
225
- if (!entry?.cacheName) return null;
226
- if ((entry.cacheExpiresAt || 0) - now <= GEMINI_GLOBAL_CACHE_MIN_LIVE_MS) {
227
- geminiGlobalCaches.delete(key);
228
- return null;
229
- }
230
- entry.lastUsedAt = now;
231
- return entry;
232
- }
233
-
234
- function _setGeminiGlobalCache(key, entry) {
235
- if (!key || !entry?.cacheName) return;
236
- geminiGlobalCaches.set(key, {
237
- ...entry,
238
- lastUsedAt: Date.now(),
239
- });
240
- _pruneGeminiGlobalCaches();
241
- }
242
-
243
- function _geminiGlobalCacheNameIsLive(cacheName, now = Date.now()) {
244
- if (!cacheName) return false;
245
- for (const entry of geminiGlobalCaches.values()) {
246
- if (entry?.cacheName === cacheName && Number(entry.cacheExpiresAt || 0) > now) return true;
247
- }
248
- return false;
249
- }
250
-
251
- function _attachGeminiCacheState(opts, entry, currentIter) {
252
- if (!entry?.cacheName) return;
253
- opts.providerState = {
254
- ...(opts.providerState || {}),
255
- gemini: {
256
- cacheName: entry.cacheName,
257
- cacheCreatedAt: entry.cacheCreatedAt,
258
- cacheCreatedAtIter: currentIter,
259
- cacheExpiresAt: entry.cacheExpiresAt,
260
- cacheModel: entry.cacheModel,
261
- cacheTokenSize: entry.cacheTokenSize,
262
- cachePrefixContentCount: entry.cachePrefixContentCount,
263
- cachePrefixHash: entry.cachePrefixHash,
264
- },
265
- };
266
- }
267
-
268
- function _resolveGeminiCacheUsage({ usageMetadata, cachedContent, providerState }) {
269
- const inputTokens = Number(usageMetadata?.promptTokenCount || usageMetadata?.totalTokenCount || 0) || 0;
270
- const reportedCachedTokens = Number(usageMetadata?.cachedContentTokenCount || 0) || 0;
271
- const cachedFallbackTokens = cachedContent
272
- ? Number(providerState?.gemini?.cacheTokenSize || 0) || 0
273
- : 0;
274
- const rawCachedTokens = reportedCachedTokens > 0 ? reportedCachedTokens : cachedFallbackTokens;
275
- const cachedTokens = inputTokens > 0 ? Math.min(rawCachedTokens, inputTokens) : rawCachedTokens;
276
- const cacheTokenSource = reportedCachedTokens > 0
277
- ? 'usage_metadata'
278
- : (cachedFallbackTokens > 0 ? 'cache_create_fallback' : 'none');
279
- return {
280
- inputTokens,
281
- reportedCachedTokens,
282
- cachedFallbackTokens,
283
- cachedTokens,
284
- cacheTokenSource,
285
- };
286
- }
287
-
288
- function writeGeminiCacheTrace({ opts, model, systemInstruction, tools, contents, usageMetadata, cachedContent }) {
289
- if (process.env.MIXDOG_GEMINI_CACHE_TRACE !== '1') return;
290
- try {
291
- const session = opts?.session || {};
292
- const {
293
- inputTokens,
294
- reportedCachedTokens,
295
- cachedFallbackTokens,
296
- cachedTokens,
297
- cacheTokenSource,
298
- } = _resolveGeminiCacheUsage({
299
- usageMetadata,
300
- cachedContent,
301
- providerState: opts?.providerState,
302
- });
303
- const toolShape = summarizeTraceTools(tools);
304
- const trace = {
305
- event: 'generate',
306
- provider: 'gemini',
307
- model,
308
- owner: session.owner || null,
309
- agent: session.agent || null,
310
- permission: session.permission || null,
311
- toolPermission: session.toolPermission || null,
312
- profileId: session.profileId || null,
313
- sourceType: session.sourceType || null,
314
- sourceName: session.sourceName || null,
315
- sessionIdHash: opts?.sessionId ? traceHash(opts.sessionId) : null,
316
- providerCacheKeyHash: opts?.providerCacheKey ? traceHash(opts.providerCacheKey) : null,
317
- promptCacheKeyHash: opts?.promptCacheKey ? traceHash(opts.promptCacheKey) : null,
318
- systemChars: systemInstruction ? systemInstruction.length : 0,
319
- systemHash: systemInstruction ? traceHash(systemInstruction) : null,
320
- toolCount: Array.isArray(tools) ? tools.length : 0,
321
- toolSchemaHash: traceHash(stableTraceStringify(toolShape)),
322
- contentTurnCount: Array.isArray(contents) ? contents.length : 0,
323
- contents: summarizeTraceContents(contents),
324
- inputTokens,
325
- cachedTokens,
326
- reportedCachedTokens,
327
- cachedFallbackTokens,
328
- cacheTokenSource,
329
- cacheAttached: !!cachedContent,
330
- cachePrefixContentCount: opts?.providerState?.gemini?.cachePrefixContentCount ?? null,
331
- cacheHitRate: inputTokens > 0 ? Number((cachedTokens / inputTokens).toFixed(6)) : null,
332
- };
333
- process.stderr.write(`[gemini-cache-trace] ${JSON.stringify(trace)}\n`);
334
- } catch (err) {
335
- process.stderr.write(`[gemini-cache-trace] failed: ${err?.message || err}\n`);
336
- }
337
- }
338
-
339
- function geminiTimeoutError(label, timeoutMs) {
340
- const err = providerTimeoutError(label, timeoutMs);
341
- err.name = 'GeminiTimeoutError';
342
- err.code = 'EGEMINITIMEOUT';
343
- return err;
344
- }
345
-
346
- function geminiTruncatedStreamError(message) {
347
- return Object.assign(
348
- new Error(message),
349
- { name: 'TruncatedStreamError', code: 'TRUNCATED_STREAM', truncatedStream: true },
350
- );
351
- }
352
-
353
- /**
354
- * Aggregate streamed GenerateContentResponse chunks into one response object
355
- * (same shape as a non-streaming generateContent JSON body).
356
- * Mirrors @google/generative-ai aggregateResponses().
357
- */
358
- function aggregateGeminiStreamChunks(responses) {
359
- const lastResponse = responses[responses.length - 1];
360
- const aggregatedResponse = {
361
- promptFeedback: lastResponse?.promptFeedback,
362
- };
363
- for (const response of responses) {
364
- if (response?.candidates) {
365
- let candidateIndex = 0;
366
- for (const candidate of response.candidates) {
367
- if (!aggregatedResponse.candidates) aggregatedResponse.candidates = [];
368
- if (!aggregatedResponse.candidates[candidateIndex]) {
369
- aggregatedResponse.candidates[candidateIndex] = { index: candidateIndex };
370
- }
371
- const aggCand = aggregatedResponse.candidates[candidateIndex];
372
- aggCand.citationMetadata = candidate.citationMetadata;
373
- aggCand.groundingMetadata = candidate.groundingMetadata;
374
- aggCand.finishReason = candidate.finishReason;
375
- aggCand.finishMessage = candidate.finishMessage;
376
- aggCand.safetyRatings = candidate.safetyRatings;
377
- if (candidate.content?.parts) {
378
- if (!aggCand.content) {
379
- aggCand.content = {
380
- role: candidate.content.role || 'user',
381
- parts: [],
382
- };
383
- }
384
- for (const part of candidate.content.parts) {
385
- const newPart = {};
386
- if (part.text) newPart.text = part.text;
387
- if (part.functionCall) newPart.functionCall = part.functionCall;
388
- if (part.thoughtSignature) newPart.thoughtSignature = part.thoughtSignature;
389
- if (part.thought_signature) newPart.thought_signature = part.thought_signature;
390
- if (part.executableCode) newPart.executableCode = part.executableCode;
391
- if (part.codeExecutionResult) newPart.codeExecutionResult = part.codeExecutionResult;
392
- if (Object.keys(newPart).length === 0) newPart.text = '';
393
- aggCand.content.parts.push(newPart);
394
- }
395
- }
396
- candidateIndex++;
397
- }
398
- }
399
- if (response?.usageMetadata) aggregatedResponse.usageMetadata = response.usageMetadata;
400
- }
401
- return aggregatedResponse;
402
- }
403
-
404
- function assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label }) {
405
- if (!sawStreamChunk) {
406
- throw geminiTruncatedStreamError(`${label} truncated: empty stream`);
407
- }
408
- if (!finishReason) {
409
- throw geminiTruncatedStreamError(`${label} truncated: no finishReason`);
410
- }
411
- }
412
-
413
- // Concatenate the text parts of a single streamed Gemini chunk. Used to feed
414
- // the gateway live-text relay (onTextDelta) with the incremental text payload
415
- // as each SSE/SDK chunk arrives. Returns '' for tool-call / thought-only /
416
- // malformed chunks so the caller can skip empty emits.
417
- function geminiChunkText(chunk) {
418
- const parts = chunk?.candidates?.[0]?.content?.parts;
419
- if (!Array.isArray(parts)) return '';
420
- let text = '';
421
- for (const p of parts) {
422
- if (p && typeof p.text === 'string') text += p.text;
423
- }
424
- return text;
425
- }
426
-
427
- function relayGeminiStreamText(t, { onTextDelta, textLeakGuard }) {
428
- if (!t) return;
429
- if (textLeakGuard) textLeakGuard.feedText(t);
430
- else if (onTextDelta) { try { onTextDelta(t); } catch {} }
431
- }
432
-
433
- /**
434
- * Rolling scanner for tool calls leaked as plain XML/antml tags inside Gemini
435
- * `part.text` streams. Mirrors the Anthropic OAuth guard: suppress tags from
436
- * visible text, synthesize known-tool calls, dispatch via onToolCall.
437
- */
438
- export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolCall, onStreamDelta }) {
439
- const _knownTools = knownToolNames instanceof Set
440
- ? knownToolNames
441
- : new Set(Array.isArray(knownToolNames) ? knownToolNames : []);
442
- const _enabled = _knownTools.size > 0;
443
- const _isKnownTool = (name) => _knownTools.has(name);
444
- let leakBuffer = '';
445
- const leakedCalls = [];
446
- const dispatchedFingerprints = new Set();
447
-
448
- const toolCallFingerprint = (name, args) => {
449
- let a = args;
450
- if (a === null || typeof a !== 'object' || Array.isArray(a)) a = {};
451
- return traceHash(stableTraceStringify({ name: name || '', args: a }));
452
- };
453
-
454
- const dispatchLeakedCall = (recovered) => {
455
- let args = recovered?.arguments;
456
- if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
457
- const fp = toolCallFingerprint(recovered.name, args);
458
- if (dispatchedFingerprints.has(fp)) return;
459
- dispatchedFingerprints.add(fp);
460
- const idHash = traceHash(stableTraceStringify({
461
- name: recovered.name,
462
- args,
463
- leak: true,
464
- })).slice(0, 16);
465
- const call = {
466
- id: `gemini_leaked_${idHash}`,
467
- name: recovered.name,
468
- arguments: args,
469
- };
470
- leakedCalls.push(call);
471
- try { onToolCall?.(call); } catch {}
472
- try { onStreamDelta?.(); } catch {}
473
- };
474
-
475
- const pumpLeakBuffer = (final) => {
476
- if (!_enabled) return;
477
- if (!leakBuffer && !final) return;
478
- const { emit, calls, rest } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final });
479
- leakBuffer = rest;
480
- if (emit && onTextDelta) {
481
- try { onTextDelta(emit); } catch {}
482
- }
483
- for (const c of calls) dispatchLeakedCall(c);
484
- };
485
-
486
- return {
487
- get enabled() { return _enabled; },
488
- feedText(text) {
489
- if (!text) return;
490
- if (!_enabled) {
491
- try { onTextDelta?.(text); } catch {}
492
- return;
493
- }
494
- leakBuffer += text;
495
- pumpLeakBuffer(false);
496
- },
497
- finalize() {
498
- pumpLeakBuffer(true);
499
- },
500
- scrubAssistantText(raw) {
501
- if (!raw) return '';
502
- if (!_enabled) return raw;
503
- const { emit, calls, rest } = scanLeakedToolCalls(raw, { isKnownTool: _isKnownTool, final: true });
504
- for (const c of calls) dispatchLeakedCall(c);
505
- return emit + rest;
506
- },
507
- filterNativeToolCalls(nativeCalls) {
508
- if (!_enabled || !nativeCalls?.length) return nativeCalls;
509
- const kept = [];
510
- for (const call of nativeCalls) {
511
- const fp = toolCallFingerprint(call?.name, call?.arguments);
512
- if (dispatchedFingerprints.has(fp)) continue;
513
- dispatchedFingerprints.add(fp);
514
- kept.push(call);
515
- }
516
- return kept.length ? kept : undefined;
517
- },
518
- getLeakedToolCalls() {
519
- return leakedCalls.length ? [...leakedCalls] : [];
520
- },
521
- };
522
- }
523
-
524
- async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
525
- if (!response?.body) throw new Error(`${label}: missing response body`);
526
- const reader = response.body.getReader();
527
- const decoder = new TextDecoder();
528
- let buffer = '';
529
- const allChunks = [];
530
- let sawStreamChunk = false;
531
- let idleTimedOut = false;
532
- let idleTimer = null;
533
- let idleReject = null;
534
-
535
- let firstByteTimer = setTimeout(() => {
536
- try { reader.cancel('first byte timeout'); } catch {}
537
- if (idleReject) {
538
- const e = geminiTimeoutError(`${label} first byte`, GEMINI_FIRST_BYTE_TIMEOUT_MS);
539
- const r = idleReject; idleReject = null; r(e);
540
- }
541
- }, GEMINI_FIRST_BYTE_TIMEOUT_MS);
542
- if (firstByteTimer.unref) firstByteTimer.unref();
543
-
544
- const clearFirstByteTimer = () => {
545
- if (firstByteTimer) {
546
- clearTimeout(firstByteTimer);
547
- firstByteTimer = null;
548
- }
549
- };
550
-
551
- const resetIdleTimer = () => {
552
- if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
553
- if (idleTimer) clearTimeout(idleTimer);
554
- idleTimer = setTimeout(() => {
555
- idleTimedOut = true;
556
- try { reader.cancel('SSE idle timeout'); } catch {}
557
- if (idleReject) {
558
- const e = geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
559
- const r = idleReject; idleReject = null; r(e);
560
- }
561
- }, PROVIDER_SSE_IDLE_TIMEOUT_MS);
562
- if (idleTimer.unref) idleTimer.unref();
563
- };
564
-
565
- const onAbort = () => {
566
- try {
567
- const c = reader.cancel('aborted');
568
- if (c && typeof c.catch === 'function') c.catch(() => {});
569
- } catch {}
570
- };
571
-
572
- if (signal) {
573
- if (signal.aborted) {
574
- const reason = signal.reason;
575
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
576
- }
577
- signal.addEventListener('abort', onAbort, { once: true });
578
- }
579
-
580
- try {
581
- resetIdleTimer();
582
- while (true) {
583
- let chunk;
584
- try {
585
- chunk = await new Promise((resolve, reject) => {
586
- idleReject = reject;
587
- reader.read().then(resolve, reject);
588
- });
589
- } catch (err) {
590
- if (idleTimedOut) {
591
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
592
- }
593
- if (signal?.aborted) {
594
- const reason = signal.reason;
595
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
596
- }
597
- throw err;
598
- } finally {
599
- idleReject = null;
600
- }
601
- const { done, value } = chunk;
602
- if (done) break;
603
- resetIdleTimer();
604
- buffer += decoder.decode(value, { stream: true });
605
- let lineEnd;
606
- while ((lineEnd = buffer.indexOf('\n')) >= 0) {
607
- let line = buffer.slice(0, lineEnd);
608
- buffer = buffer.slice(lineEnd + 1);
609
- line = line.replace(/\r$/, '');
610
- if (!line.startsWith('data: ')) continue;
611
- const data = line.slice(6).trim();
612
- if (!data || data === '[DONE]') continue;
613
- let parsed;
614
- try { parsed = JSON.parse(data); } catch { continue; }
615
- if (!sawStreamChunk) {
616
- sawStreamChunk = true;
617
- clearFirstByteTimer();
618
- }
619
- allChunks.push(parsed);
620
- try { onStreamDelta?.(); } catch {}
621
- if (onTextDelta || textLeakGuard) {
622
- const t = geminiChunkText(parsed);
623
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
624
- }
625
- }
626
- }
627
- if (buffer.trim()) {
628
- const line = buffer.trim().replace(/\r$/, '');
629
- if (line.startsWith('data: ')) {
630
- const data = line.slice(6).trim();
631
- if (data && data !== '[DONE]') {
632
- try {
633
- const parsed = JSON.parse(data);
634
- if (!sawStreamChunk) {
635
- sawStreamChunk = true;
636
- clearFirstByteTimer();
637
- }
638
- allChunks.push(parsed);
639
- try { onStreamDelta?.(); } catch {}
640
- if (onTextDelta || textLeakGuard) {
641
- const t = geminiChunkText(parsed);
642
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
643
- }
644
- } catch { /* skip malformed tail */ }
645
- }
646
- }
647
- }
648
- } finally {
649
- clearFirstByteTimer();
650
- if (idleTimer) clearTimeout(idleTimer);
651
- if (signal) signal.removeEventListener('abort', onAbort);
652
- try { reader.releaseLock(); } catch {}
653
- try { textLeakGuard?.finalize(); } catch {}
654
- }
655
-
656
- const aggregated = aggregateGeminiStreamChunks(allChunks);
657
- const finishReason = aggregated.candidates?.[0]?.finishReason || null;
658
- assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
659
- return aggregated;
660
- }
661
-
662
- async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
663
- let sawStreamChunk = false;
664
- let idleTimedOut = false;
665
- let idleTimer = null;
666
- let firstByteReject = null;
667
- let firstByteTimer = null;
668
- let inFlightReject = null;
669
-
670
- const armFirstByteTimer = () => {
671
- if (firstByteTimer) clearTimeout(firstByteTimer);
672
- firstByteTimer = setTimeout(() => {
673
- if (firstByteReject) {
674
- const e = geminiTimeoutError(`${label} first byte`, GEMINI_FIRST_BYTE_TIMEOUT_MS);
675
- const r = firstByteReject; firstByteReject = null; r(e);
676
- }
677
- }, GEMINI_FIRST_BYTE_TIMEOUT_MS);
678
- if (firstByteTimer.unref) firstByteTimer.unref();
679
- };
680
-
681
- const clearFirstByteTimer = () => {
682
- if (firstByteTimer) {
683
- clearTimeout(firstByteTimer);
684
- firstByteTimer = null;
685
- }
686
- firstByteReject = null;
687
- };
688
-
689
- const resetIdleTimer = () => {
690
- if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
691
- if (idleTimer) clearTimeout(idleTimer);
692
- idleTimer = setTimeout(() => {
693
- idleTimedOut = true;
694
- if (inFlightReject) {
695
- const e = geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
696
- const r = inFlightReject; inFlightReject = null; r(e);
697
- }
698
- }, PROVIDER_SSE_IDLE_TIMEOUT_MS);
699
- if (idleTimer.unref) idleTimer.unref();
700
- };
701
-
702
- if (signal?.aborted) {
703
- const reason = signal.reason;
704
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
705
- }
706
-
707
- const iterator = streamResult.stream[Symbol.asyncIterator]();
708
-
709
- // Wire the abort signal to actually CANCEL iteration (mirror of the REST
710
- // consumer's onAbort -> reader.cancel). Without this, a parent / client /
711
- // gateway abort that fires AFTER the first byte — with the SSE idle
712
- // watchdog off by default — would leave iterator.next() hanging: the loop
713
- // below only reads `signal` for error translation, never to interrupt the
714
- // pending read. Rejecting the in-flight promise and returning the iterator
715
- // releases it promptly even if the SDK is slow to propagate the underlying
716
- // request abort.
717
- let onSignalAbort = null;
718
- if (signal) {
719
- onSignalAbort = () => {
720
- const reason = signal.reason;
721
- const err = reason instanceof Error ? reason : new Error(`${label} aborted`);
722
- if (inFlightReject) {
723
- const r = inFlightReject;
724
- inFlightReject = null;
725
- firstByteReject = null;
726
- r(err);
727
- }
728
- try {
729
- const ret = iterator.return?.();
730
- if (ret && typeof ret.catch === 'function') ret.catch(() => {});
731
- } catch {}
732
- };
733
- signal.addEventListener('abort', onSignalAbort, { once: true });
734
- }
735
-
736
- try {
737
- armFirstByteTimer();
738
- resetIdleTimer();
739
- while (true) {
740
- if (idleTimedOut) {
741
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
742
- }
743
- let step;
744
- try {
745
- step = await new Promise((resolve, reject) => {
746
- inFlightReject = reject;
747
- if (!sawStreamChunk) firstByteReject = reject;
748
- iterator.next().then(
749
- (value) => {
750
- inFlightReject = null;
751
- firstByteReject = null;
752
- resolve(value);
753
- },
754
- (err) => {
755
- inFlightReject = null;
756
- firstByteReject = null;
757
- reject(err);
758
- },
759
- );
760
- });
761
- } catch (err) {
762
- if (idleTimedOut) {
763
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
764
- }
765
- if (signal?.aborted) {
766
- const reason = signal.reason;
767
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
768
- }
769
- throw err;
770
- }
771
- if (step.done) break;
772
- if (!sawStreamChunk) {
773
- sawStreamChunk = true;
774
- clearFirstByteTimer();
775
- }
776
- resetIdleTimer();
777
- try { onStreamDelta?.(); } catch {}
778
- if (onTextDelta || textLeakGuard) {
779
- const t = geminiChunkText(step.value);
780
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
781
- }
782
- }
783
- if (idleTimedOut) {
784
- throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
785
- }
786
- } catch (err) {
787
- clearFirstByteTimer();
788
- if (signal?.aborted) {
789
- const reason = signal.reason;
790
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
791
- }
792
- throw err;
793
- } finally {
794
- clearFirstByteTimer();
795
- if (idleTimer) clearTimeout(idleTimer);
796
- if (signal && onSignalAbort) {
797
- try { signal.removeEventListener('abort', onSignalAbort); } catch {}
798
- }
799
- try { textLeakGuard?.finalize(); } catch {}
800
- }
801
-
802
- let response;
803
- try {
804
- response = await streamResult.response;
805
- } catch (err) {
806
- if (signal?.aborted) {
807
- const reason = signal.reason;
808
- throw reason instanceof Error ? reason : new Error(`${label} aborted`);
809
- }
810
- throw err;
811
- }
812
- const raw = response?.candidates ? response : (response?.response || response);
813
- const finishReason = raw?.candidates?.[0]?.finishReason || null;
814
- assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
815
- return raw;
816
- }
817
-
818
- /**
819
- * Convert JSON Schema type string to Gemini SchemaType.
820
- * Gemini SDK uses its own enum instead of plain strings.
821
- */
822
- function toSchemaType(t) {
823
- const map = {
824
- string: SchemaType.STRING,
825
- number: SchemaType.NUMBER,
826
- integer: SchemaType.INTEGER,
827
- boolean: SchemaType.BOOLEAN,
828
- array: SchemaType.ARRAY,
829
- object: SchemaType.OBJECT,
830
- };
831
- return map[t] ?? SchemaType.STRING;
832
- }
833
-
834
- /**
835
- * Recursively convert a JSON Schema object to Gemini's FunctionDeclarationSchema.
836
- * Gemini requires `type` to be a SchemaType enum, not a plain string, and
837
- * rejects several JSON Schema fields the API does not understand
838
- * (additionalProperties, $schema, $ref, const, examples, definitions,
839
- * patternProperties). We strip those at every level.
840
- */
841
- const GEMINI_SCHEMA_STRIP = new Set([
842
- 'additionalProperties',
843
- '$schema',
844
- '$ref',
845
- 'const',
846
- 'examples',
847
- 'definitions',
848
- 'patternProperties',
849
- ]);
850
- function convertSchema(schema) {
851
- if (!schema || typeof schema !== 'object') return schema;
852
- const result = {};
853
- for (const [k, v] of Object.entries(schema)) {
854
- if (GEMINI_SCHEMA_STRIP.has(k)) continue;
855
- result[k] = v;
856
- }
857
- // Gemini's Schema validator requires every `enum` entry to be a string,
858
- // even when the parent `type` is integer/number/boolean. Drop the enum in
859
- // that case rather than emit an invalid typed enum — `type` plus the
860
- // description still guides the model, and the tool handler revalidates.
861
- const rawType = typeof result.type === 'string' ? result.type : undefined;
862
- if (Array.isArray(result.enum) && (rawType === 'integer' || rawType === 'number' || rawType === 'boolean')) {
863
- if (result.enum.some((item) => typeof item !== 'string')) {
864
- delete result.enum;
865
- }
866
- }
867
- // Gemini rejects array schemas that omit `items`; fill a permissive
868
- // default so the declaration validates.
869
- if (rawType === 'array' && (!result.items || typeof result.items !== 'object')) {
870
- result.items = { type: 'string' };
871
- }
872
- if (typeof result.type === 'string') {
873
- result.type = toSchemaType(result.type);
874
- }
875
- if (result.properties && typeof result.properties === 'object') {
876
- const props = {};
877
- for (const [key, val] of Object.entries(result.properties)) {
878
- props[key] = convertSchema(val);
879
- }
880
- result.properties = props;
881
- }
882
- if (result.items && typeof result.items === 'object') {
883
- result.items = convertSchema(result.items);
884
- }
885
- // Recurse into JSON Schema combinator keys so disallowed fields
886
- // (additionalProperties, $schema, etc.) get stripped at every nesting
887
- // level. Without this, schemas using anyOf/oneOf/allOf/not pass the
888
- // shallow strip but fail Gemini validation at depth.
889
- //
890
- // Two Gemini-specific normalizations are also applied per combinator
891
- // subschema:
892
- // 1. Inject `type: OBJECT` when a subschema uses object-only keys
893
- // (`required` / `properties`) without an explicit type — Gemini
894
- // rejects `required` outside of OBJECT type.
895
- // 2. Materialize a local `properties` map from the parent's properties
896
- // when the subschema only carries `required: [names]` — Gemini
897
- // validates that every name in `required` exists in *this*
898
- // subschema's `properties` (it does not inherit from the parent
899
- // the way JSON Schema's compositional model does).
900
- for (const combinator of ['anyOf', 'oneOf', 'allOf']) {
901
- if (Array.isArray(result[combinator])) {
902
- result[combinator] = result[combinator].map((s) => {
903
- const sub = convertSchema(s);
904
- if (sub && typeof sub === 'object') {
905
- const usesObjectKeys = sub.required !== undefined || sub.properties !== undefined;
906
- if (usesObjectKeys && sub.type === undefined) {
907
- sub.type = toSchemaType('object');
908
- }
909
- if (Array.isArray(sub.required) && !sub.properties && result.properties) {
910
- const projected = {};
911
- for (const k of sub.required) {
912
- if (result.properties[k]) projected[k] = result.properties[k];
913
- }
914
- if (Object.keys(projected).length > 0) sub.properties = projected;
915
- }
916
- }
917
- return sub;
918
- });
919
- }
920
- }
921
- if (result.not && typeof result.not === 'object') {
922
- result.not = convertSchema(result.not);
923
- }
924
- return result;
925
- }
926
-
927
- function toGeminiTools(tools) {
928
- return {
929
- functionDeclarations: tools.map((t) => ({
930
- name: t.name,
931
- description: t.description,
932
- parameters: convertSchema(t.inputSchema),
933
- })),
934
- };
935
- }
936
- function toGeminiNativeTools(nativeTools) {
937
- if (!Array.isArray(nativeTools)) return [];
938
- const out = [];
939
- for (const tool of nativeTools) {
940
- const type = String(tool?.type || '').trim().toLowerCase();
941
- if (type === 'google_search' || type === 'google_search_retrieval') {
942
- out.push({ googleSearch: {} });
943
- }
944
- }
945
- return out;
946
- }
947
-
948
- function collectGeminiGroundingSources(candidate) {
949
- const out = [];
950
- const seen = new Set();
951
- const add = (source) => {
952
- if (!source || typeof source !== 'object') return;
953
- const web = source.web && typeof source.web === 'object' ? source.web : source;
954
- const url = String(web.uri || web.url || source.uri || source.url || '').trim();
955
- if (!url || seen.has(url)) return;
956
- seen.add(url);
957
- out.push({
958
- title: String(web.title || source.title || url).trim(),
959
- url,
960
- snippet: '',
961
- source: 'gemini-grounding',
962
- provider: 'gemini',
963
- });
964
- };
965
- const grounding = candidate?.groundingMetadata || {};
966
- for (const chunk of Array.isArray(grounding.groundingChunks) ? grounding.groundingChunks : []) add(chunk);
967
- const citationMetadata = candidate?.citationMetadata || {};
968
- for (const source of Array.isArray(citationMetadata.citationSources) ? citationMetadata.citationSources : []) add(source);
969
- return out;
970
- }
971
-
972
- // Map the orchestrator-level toolChoice to Gemini's functionCallingConfig.
973
- // auto -> AUTO
974
- // required -> ANY
975
- // none -> NONE
976
- // { name } -> ANY + allowedFunctionNames:[name] (specific tool)
977
- function toGeminiToolConfig(toolChoice) {
978
- if (toolChoice == null) return undefined;
979
- if (typeof toolChoice === 'string') {
980
- if (toolChoice === 'auto') return { functionCallingConfig: { mode: 'AUTO' } };
981
- if (toolChoice === 'required') return { functionCallingConfig: { mode: 'ANY' } };
982
- if (toolChoice === 'none') return { functionCallingConfig: { mode: 'NONE' } };
983
- return undefined;
984
- }
985
- if (typeof toolChoice === 'object') {
986
- const name = toolChoice.name || toolChoice.function?.name;
987
- if (typeof name === 'string' && name) {
988
- return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [name] } };
989
- }
990
- }
991
- return undefined;
992
- }
993
-
994
- function toGeminiContent(message, toolNameByCallId) {
995
- if (!message || message.role === 'system') return null;
996
- if (message.role === 'assistant' && message.toolCalls?.length) {
997
- const parts = [];
998
- if (message.content) parts.push(...normalizeContentForGeminiParts(message.content));
999
- for (const tc of message.toolCalls) {
1000
- // Gemini 3 thinking models require the original thoughtSignature
1001
- // echoed back on every prior functionCall so the cached thinking
1002
- // prefix stays valid. v1beta places the field at the Part level
1003
- // (sibling of functionCall) — putting it inside functionCall returns
1004
- // 400 "Unknown name". Older models / first turn have no signature.
1005
- const part = { functionCall: { name: tc.name, args: tc.arguments } };
1006
- if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;
1007
- parts.push(part);
1008
- }
1009
- return { role: 'model', parts };
1010
- }
1011
- if (message.role === 'tool') {
1012
- // Tool result content stays byte-identical for cache prefix stability.
1013
- // Gemini accepts functionResponse parts under role 'user' (per docs).
1014
- // Using 'user' keeps tool_result entries byte-identical between
1015
- // cachedContents.create (which rejects role:'function') and
1016
- // generateContent, so the cached prefix actually matches at runtime.
1017
- // functionResponse.name must be the FUNCTION name, not the synthetic
1018
- // toolCallId. Resolve it from the toolCallId->functionName map built
1019
- // from prior assistant tool_calls; fall back to the raw id only when
1020
- // no mapping exists.
1021
- const functionName = (toolNameByCallId && toolNameByCallId.get(message.toolCallId))
1022
- || message.toolCallId
1023
- || '';
1024
- const { response, mediaParts } = splitToolContentForGemini(message.content);
1025
- const parts = [{ functionResponse: { name: functionName, response } }];
1026
- if (mediaParts.length) parts.push(...mediaParts);
1027
- return {
1028
- role: 'user',
1029
- parts,
1030
- };
1031
- }
1032
- return {
1033
- role: message.role === 'assistant' ? 'model' : 'user',
1034
- parts: normalizeContentForGeminiParts(message.content),
1035
- };
1036
- }
1037
-
1038
- function toGeminiContents(messages) {
1039
- const contents = [];
1040
- // Map synthetic toolCallId -> function name from prior assistant
1041
- // tool_calls so each functionResponse part carries the real function name.
1042
- const toolNameByCallId = new Map();
1043
- for (const m of messages) {
1044
- if (m?.role === 'assistant' && Array.isArray(m.toolCalls)) {
1045
- for (const tc of m.toolCalls) {
1046
- if (tc?.id && tc?.name) toolNameByCallId.set(tc.id, tc.name);
1047
- }
1048
- }
1049
- }
1050
- for (const message of messages) {
1051
- const content = toGeminiContent(message, toolNameByCallId);
1052
- if (content) contents.push(content);
1053
- }
1054
- return contents;
1055
- }
1056
-
1057
- export function parseToolCalls(parts) {
1058
- const calls = parts.filter((p) => 'functionCall' in p && !!p.functionCall);
1059
- if (!calls.length)
1060
- return undefined;
1061
- // The @google/generative-ai 0.24.1 SDK predates Gemini 3 thinking — its
1062
- // FunctionCall type only declares { name, args }. The runtime object,
1063
- // however, retains whatever the wire response carried, which means the
1064
- // signature may sit under any of:
1065
- // • part.functionCall.thoughtSignature (camelCase, expected)
1066
- // • part.functionCall.thought_signature (snake_case, raw protobuf)
1067
- // • part.thoughtSignature / part.thought_signature (sibling on Part)
1068
- // Read all four and use the first non-empty hit. Set MIXDOG_DEBUG_GEMINI=1
1069
- // to dump the raw parts so we can confirm the actual key location on the
1070
- // next session and harden the parser.
1071
- if (process.env.MIXDOG_DEBUG_GEMINI === '1') {
1072
- try { process.stderr.write(`[gemini fc raw] ${JSON.stringify(parts)}\n`); } catch {}
1073
- }
1074
- return calls.map((p, i) => {
1075
- const fc = p.functionCall;
1076
- const sig = fc.thoughtSignature
1077
- || fc.thought_signature
1078
- || p.thoughtSignature
1079
- || p.thought_signature
1080
- || null;
1081
- const idHash = traceHash(stableTraceStringify({
1082
- index: i,
1083
- name: fc.name || '',
1084
- args: fc.args ?? {},
1085
- })).slice(0, 16);
1086
- const call = {
1087
- id: `gemini_${idHash}`,
1088
- name: fc.name,
1089
- arguments: (fc.args ?? {}),
1090
- };
1091
- if (sig) call.thoughtSignature = sig;
1092
- return call;
1093
- });
1094
- }
1095
-
1096
- function emitGeminiToolCalls(toolCalls, onToolCall) {
1097
- if (typeof onToolCall !== 'function' || !Array.isArray(toolCalls)) return;
1098
- const emitted = new Set();
1099
- for (const call of toolCalls) {
1100
- if (!call?.id || !call?.name || emitted.has(call.id)) continue;
1101
- emitted.add(call.id);
1102
- try { onToolCall(call); } catch {}
1103
- }
1104
- }
131
+ // --- Cache accounting/trace: extracted to gemini-cache.mjs ---
132
+ // --- Stream consumption/guards: extracted to gemini-stream.mjs ---
133
+ // --- Schema/content/tool-call mapping: extracted to gemini-schema.mjs ---
1105
134
 
1106
135
  export class GeminiProvider {
1107
136
  // promptTokenCount is the total (cachedContentTokenCount is a subset), so
@@ -1851,7 +880,8 @@ export class GeminiProvider {
1851
880
  _markLatestGemini(normalized);
1852
881
  // LiteLLM catalog overlays pricing and updated metadata.
1853
882
  const { enrichModels } = await import('./model-catalog.mjs');
1854
- const enriched = await enrichModels(normalized);
883
+ const { sanitizeModelList } = await import('./model-list-sanitize.mjs');
884
+ const enriched = sanitizeModelList(await enrichModels(normalized), { provider: 'gemini' });
1855
885
  _modelCache.save(enriched);
1856
886
  return enriched;
1857
887
  }