mixdog 0.9.63 → 0.9.65

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 (408) hide show
  1. package/README.md +3 -0
  2. package/package.json +9 -2
  3. package/scripts/code-graph-description-contract.mjs +3 -3
  4. package/scripts/verify-release-assets.mjs +2 -2
  5. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +11 -14
  6. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -6
  7. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  8. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
  9. package/src/runtime/agent/orchestrator/config.mjs +19 -124
  10. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -6
  11. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/internal-agents.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +4 -4
  15. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +4 -4
  17. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +2 -2
  18. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +4 -4
  19. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +333 -0
  20. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -4
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -277
  24. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -6
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +196 -0
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +460 -0
  30. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +14 -593
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +4 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -280
  37. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +357 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +216 -0
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +2 -188
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/registry.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -2
  46. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +3 -3
  49. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +2 -2
  50. package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +2 -2
  54. package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -4
  55. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +1 -1
  56. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +17 -0
  57. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +18 -26
  59. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
  60. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +3 -3
  61. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -0
  62. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +1 -1
  64. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +49 -0
  66. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +260 -0
  68. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +72 -0
  69. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +163 -0
  70. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -1
  73. package/src/runtime/agent/orchestrator/session/store.mjs +9 -561
  74. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/stall-policy.mjs +11 -11
  77. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -1
  78. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -154
  83. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/path-locks.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -2
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +2 -3
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +4 -4
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -5
  97. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +2 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +519 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +5 -449
  102. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +212 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -1
  105. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +1 -7
  106. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +9 -36
  107. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +15 -3
  108. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +429 -0
  109. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -395
  110. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +2 -2
  111. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +1 -1
  112. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +3 -3
  115. package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +5 -5
  116. package/src/runtime/agent/orchestrator/tools/next-call-utils.mjs +1 -1
  117. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +7 -7
  119. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +72 -9
  120. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +4 -4
  121. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  123. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -4
  124. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  125. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +3 -3
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -444
  127. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +505 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +2 -2
  129. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +1 -1
  130. package/src/runtime/channels/lib/backend-dispatch.mjs +20 -5
  131. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  132. package/src/runtime/channels/lib/telegram-format.mjs +1 -1
  133. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -4
  134. package/src/runtime/channels/tool-defs.mjs +2 -3
  135. package/src/runtime/memory/index.mjs +1 -5
  136. package/src/runtime/memory/lib/core-memory-candidates.mjs +357 -0
  137. package/src/runtime/memory/lib/core-memory-store.mjs +7 -357
  138. package/src/runtime/memory/lib/cycle-signatures.mjs +1 -1
  139. package/src/runtime/memory/lib/embedding-model-config.mjs +1 -1
  140. package/src/runtime/memory/lib/embedding-provider.mjs +1 -5
  141. package/src/runtime/memory/lib/embedding-worker.mjs +1 -5
  142. package/src/runtime/memory/lib/ko-morph.mjs +2 -2
  143. package/src/runtime/memory/lib/memory-config-flags.mjs +2 -5
  144. package/src/runtime/memory/lib/memory-cycle-requests.mjs +3 -7
  145. package/src/runtime/memory/lib/memory-cycle1.mjs +4 -8
  146. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -1
  147. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -1
  148. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +2 -5
  149. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -5
  150. package/src/runtime/memory/lib/memory-embed.mjs +2 -6
  151. package/src/runtime/memory/lib/memory-log.mjs +8 -0
  152. package/src/runtime/memory/lib/memory-ops-policy.mjs +4 -8
  153. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +1 -1
  154. package/src/runtime/memory/lib/memory-recall-store.mjs +2 -193
  155. package/src/runtime/memory/lib/memory-score.mjs +1 -1
  156. package/src/runtime/memory/lib/memory-text-utils.mjs +2 -2
  157. package/src/runtime/memory/lib/memory.mjs +2 -6
  158. package/src/runtime/memory/lib/pg/adapter.mjs +2 -6
  159. package/src/runtime/memory/lib/pg/process.mjs +1 -5
  160. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -5
  161. package/src/runtime/memory/lib/recall-format.mjs +2 -2
  162. package/src/runtime/memory/lib/recall-scoring.mjs +195 -0
  163. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -5
  164. package/src/runtime/memory/lib/trace-store.mjs +2 -6
  165. package/src/runtime/search/lib/http-fetch.mjs +6 -6
  166. package/src/runtime/search/lib/web-tools.mjs +2 -2
  167. package/src/runtime/shared/buffered-appender.mjs +1 -1
  168. package/src/runtime/shared/child-guardian.mjs +1 -1
  169. package/src/runtime/shared/child-spawn-gate.mjs +1 -1
  170. package/src/runtime/shared/config.mjs +4 -4
  171. package/src/runtime/shared/launcher-control.mjs +9 -10
  172. package/src/runtime/shared/llm/index.mjs +5 -20
  173. package/src/runtime/shared/markdown-frontmatter.mjs +1 -1
  174. package/src/runtime/shared/memory-snapshot.mjs +4 -4
  175. package/src/runtime/shared/pristine-execution.mjs +1 -1
  176. package/src/runtime/shared/process-lifecycle.mjs +0 -1
  177. package/src/runtime/shared/process-shutdown.mjs +1 -1
  178. package/src/runtime/shared/resource-admission.mjs +2 -2
  179. package/src/runtime/shared/schedule-session-run.mjs +1 -1
  180. package/src/runtime/shared/schedules-db.mjs +1 -1
  181. package/src/runtime/shared/service-discovery.mjs +2 -2
  182. package/src/runtime/shared/staged-update.mjs +6 -6
  183. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  184. package/src/runtime/shared/tool-primitives.mjs +1 -1
  185. package/src/runtime/shared/tool-surface.mjs +2 -2
  186. package/src/runtime/shared/turn-snapshot.mjs +184 -0
  187. package/src/runtime/shared/update-checker.mjs +1 -1
  188. package/src/runtime/shared/user-cwd.mjs +3 -3
  189. package/src/runtime/shared/user-data-guard.mjs +4 -4
  190. package/src/runtime/shared/workspace-router.mjs +5 -5
  191. package/src/session-runtime/config-helpers.mjs +2 -3
  192. package/src/session-runtime/config-lifecycle.mjs +1 -1
  193. package/src/session-runtime/hitch-profile.mjs +1 -1
  194. package/src/session-runtime/lifecycle-api.mjs +6 -4
  195. package/src/session-runtime/model-recency.mjs +5 -5
  196. package/src/session-runtime/plugin-mcp.mjs +1 -1
  197. package/src/session-runtime/provider-request-snapshot.mjs +338 -0
  198. package/src/session-runtime/runtime-core.mjs +4 -0
  199. package/src/session-runtime/session-text.mjs +4 -4
  200. package/src/session-runtime/session-turn-api.mjs +7 -0
  201. package/src/session-runtime/tool-catalog-schema.mjs +197 -0
  202. package/src/session-runtime/tool-catalog.mjs +6 -483
  203. package/src/session-runtime/tool-defs.mjs +1 -1
  204. package/src/standalone/agent-task-status.mjs +1 -1
  205. package/src/standalone/agent-tool/helpers.mjs +2 -2
  206. package/src/standalone/channel-admin.mjs +4 -4
  207. package/src/standalone/explore-tool.mjs +3 -3
  208. package/src/standalone/hook-bus/config.mjs +1 -1
  209. package/src/standalone/hook-bus/constants.mjs +1 -1
  210. package/src/standalone/hook-bus/rules.mjs +1 -1
  211. package/src/standalone/projects.mjs +1 -1
  212. package/src/standalone/provider-admin.mjs +2 -2
  213. package/src/tui/App.jsx +1 -1
  214. package/src/tui/app/app-format.mjs +2 -2
  215. package/src/tui/app/clipboard.mjs +3 -3
  216. package/src/tui/app/input-parsers.mjs +1 -1
  217. package/src/tui/app/text-layout.mjs +1 -1
  218. package/src/tui/app/transcript-row-estimate.mjs +360 -0
  219. package/src/tui/app/transcript-window.mjs +5 -347
  220. package/src/tui/app/use-mouse-input.mjs +1 -1
  221. package/src/tui/app/use-prompt-handlers.mjs +1 -1
  222. package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
  223. package/src/tui/components/tool-execution/text-format.mjs +2 -2
  224. package/src/tui/components/tool-output-format.mjs +7 -7
  225. package/src/tui/display-width.mjs +4 -4
  226. package/src/tui/dist/index.mjs +187 -99
  227. package/src/tui/engine/agent-envelope.mjs +5 -5
  228. package/src/tui/engine/live-share.mjs +100 -0
  229. package/src/tui/engine/notification-plan.mjs +1 -1
  230. package/src/tui/engine/queue-helpers.mjs +3 -3
  231. package/src/tui/engine/render-timing.mjs +4 -5
  232. package/src/tui/engine/session-api-ext.mjs +6 -3
  233. package/src/tui/engine/tool-result-status.mjs +5 -5
  234. package/src/tui/engine/tool-result-text.mjs +1 -1
  235. package/src/tui/engine/transcript-spill.mjs +585 -0
  236. package/src/tui/engine/tui-steering-persist.mjs +1 -1
  237. package/src/tui/engine.mjs +29 -474
  238. package/src/tui/lib/voice-setup.mjs +1 -1
  239. package/src/tui/markdown/format-token.mjs +1 -1
  240. package/src/tui/markdown/render-ansi.mjs +1 -1
  241. package/src/tui/markdown/table-layout.mjs +4 -4
  242. package/src/tui/paste-attachments.mjs +1 -1
  243. package/src/tui/prompt-history-store.mjs +3 -3
  244. package/src/tui/statusline-ansi-bridge.mjs +2 -2
  245. package/src/tui/theme.mjs +1 -1
  246. package/src/tui/transcript-tool-failures.mjs +2 -2
  247. package/src/ui/ansi.mjs +0 -9
  248. package/src/ui/statusline-format.mjs +2 -4
  249. package/scripts/_bench-cwc.json +0 -20
  250. package/scripts/_jitter-fuzz.jsx +0 -42
  251. package/scripts/_jitter-fuzz2.jsx +0 -30
  252. package/scripts/_jitter-probe.jsx +0 -35
  253. package/scripts/_jp2.jsx +0 -16
  254. package/scripts/_smoke_wd.mjs +0 -7
  255. package/scripts/abort-recovery-test.mjs +0 -175
  256. package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
  257. package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
  258. package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
  259. package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
  260. package/scripts/agent-loop-policy-test.mjs +0 -37
  261. package/scripts/agent-model-liveness-test.mjs +0 -763
  262. package/scripts/agent-parallel-smoke.mjs +0 -435
  263. package/scripts/agent-route-batch-test.mjs +0 -40
  264. package/scripts/agent-tag-reuse-smoke.mjs +0 -441
  265. package/scripts/agent-terminal-reap-test.mjs +0 -252
  266. package/scripts/agent-trace-io-test.mjs +0 -133
  267. package/scripts/ansi-color-capability-test.mjs +0 -90
  268. package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
  269. package/scripts/anthropic-maxtokens-test.mjs +0 -119
  270. package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
  271. package/scripts/anthropic-transport-policy-test.mjs +0 -726
  272. package/scripts/apply-patch-edit-smoke.mjs +0 -71
  273. package/scripts/arg-guard-test.mjs +0 -93
  274. package/scripts/async-notify-settlement-test.mjs +0 -99
  275. package/scripts/atomic-lock-tryonce-test.mjs +0 -125
  276. package/scripts/background-task-meta-smoke.mjs +0 -38
  277. package/scripts/bench-run.mjs +0 -508
  278. package/scripts/boot-smoke.mjs +0 -137
  279. package/scripts/build-runtime-windows.ps1 +0 -242
  280. package/scripts/channel-daemon-smoke.mjs +0 -1103
  281. package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
  282. package/scripts/code-graph-disk-hit-test.mjs +0 -290
  283. package/scripts/code-graph-dispatch-test.mjs +0 -96
  284. package/scripts/code-graph-root-federation-test.mjs +0 -273
  285. package/scripts/compact-active-turn-test.mjs +0 -68
  286. package/scripts/compact-file-reattach-test.mjs +0 -88
  287. package/scripts/compact-pressure-test.mjs +0 -788
  288. package/scripts/compact-prior-context-flatten-test.mjs +0 -252
  289. package/scripts/compact-recall-digest-test.mjs +0 -57
  290. package/scripts/compact-smoke.mjs +0 -1017
  291. package/scripts/compact-trigger-migration-smoke.mjs +0 -261
  292. package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
  293. package/scripts/context-mcp-metering-test.mjs +0 -1406
  294. package/scripts/debounced-skills-async-save-test.mjs +0 -57
  295. package/scripts/deferred-tool-loading-test.mjs +0 -250
  296. package/scripts/desktop-session-bridge-test.mjs +0 -1013
  297. package/scripts/dispatch-persist-recovery-test.mjs +0 -141
  298. package/scripts/embedding-worker-exit-test.mjs +0 -76
  299. package/scripts/execution-completion-dedup-test.mjs +0 -205
  300. package/scripts/execution-pending-resume-kick-test.mjs +0 -151
  301. package/scripts/execution-resume-esc-integration-test.mjs +0 -176
  302. package/scripts/explore-bench-tmp.mjs +0 -36
  303. package/scripts/explore-bench.mjs +0 -248
  304. package/scripts/explore-prompt-policy-test.mjs +0 -256
  305. package/scripts/explore-timeout-cancel-test.mjs +0 -345
  306. package/scripts/find-fuzzy-hidden-test.mjs +0 -267
  307. package/scripts/forwarder-rebind-tail-test.mjs +0 -67
  308. package/scripts/freevar-smoke.mjs +0 -98
  309. package/scripts/gemini-provider-test.mjs +0 -1448
  310. package/scripts/generate-oc-icons.mjs +0 -11
  311. package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
  312. package/scripts/headless-pristine-execution-test.mjs +0 -614
  313. package/scripts/hook-bus-test.mjs +0 -552
  314. package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
  315. package/scripts/internal-comms-bench-test.mjs +0 -226
  316. package/scripts/internal-comms-bench.mjs +0 -853
  317. package/scripts/internal-comms-smoke.mjs +0 -277
  318. package/scripts/internal-tools-normalization-test.mjs +0 -62
  319. package/scripts/interrupted-turn-history-test.mjs +0 -399
  320. package/scripts/lead-workflow-smoke.mjs +0 -592
  321. package/scripts/lifecycle-api-test.mjs +0 -137
  322. package/scripts/live-share-test.mjs +0 -148
  323. package/scripts/live-worker-smoke.mjs +0 -333
  324. package/scripts/log-writer-guard-smoke.mjs +0 -131
  325. package/scripts/maintenance-default-routes-test.mjs +0 -164
  326. package/scripts/max-output-recovery-persist-test.mjs +0 -81
  327. package/scripts/max-output-recovery-test.mjs +0 -368
  328. package/scripts/mcp-client-normalization-test.mjs +0 -45
  329. package/scripts/mcp-grace-deferred-test.mjs +0 -225
  330. package/scripts/memory-core-input-test.mjs +0 -167
  331. package/scripts/memory-cycle-routing-test.mjs +0 -111
  332. package/scripts/memory-meta-concurrency-test.mjs +0 -20
  333. package/scripts/memory-pg-recovery-test.mjs +0 -59
  334. package/scripts/memory-rule-contract-test.mjs +0 -93
  335. package/scripts/model-list-sanitize-test.mjs +0 -37
  336. package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
  337. package/scripts/native-edit-wire-test.mjs +0 -152
  338. package/scripts/notify-completion-mirror-test.mjs +0 -73
  339. package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
  340. package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
  341. package/scripts/openai-ws-early-settle-test.mjs +0 -216
  342. package/scripts/output-style-bench.mjs +0 -292
  343. package/scripts/output-style-smoke.mjs +0 -145
  344. package/scripts/parent-abort-link-test.mjs +0 -90
  345. package/scripts/patch-binary-cache-test.mjs +0 -181
  346. package/scripts/path-suffix-test.mjs +0 -57
  347. package/scripts/pending-completion-drop-test.mjs +0 -239
  348. package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
  349. package/scripts/pretool-ask-runtime-test.mjs +0 -54
  350. package/scripts/process-lifecycle-test.mjs +0 -461
  351. package/scripts/prompt-immediate-render-test.mjs +0 -89
  352. package/scripts/prompt-input-parity-test.mjs +0 -145
  353. package/scripts/provider-admission-scheduler-test.mjs +0 -681
  354. package/scripts/provider-contract-test.mjs +0 -632
  355. package/scripts/provider-stream-stall-test.mjs +0 -276
  356. package/scripts/provider-toolcall-test.mjs +0 -3063
  357. package/scripts/reactive-compact-persist-smoke.mjs +0 -203
  358. package/scripts/recall-bench.mjs +0 -359
  359. package/scripts/resource-admission-test.mjs +0 -792
  360. package/scripts/result-classification-test.mjs +0 -75
  361. package/scripts/rg-runner-test.mjs +0 -240
  362. package/scripts/routing-corpus-test.mjs +0 -349
  363. package/scripts/sanitize-tool-pairs-test.mjs +0 -260
  364. package/scripts/session-bench-cache-break-test.mjs +0 -102
  365. package/scripts/session-bench.mjs +0 -1708
  366. package/scripts/session-context-bench.mjs +0 -344
  367. package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
  368. package/scripts/session-ingest-compaction-smoke.mjs +0 -279
  369. package/scripts/session-ingest-smoke.mjs +0 -177
  370. package/scripts/session-orphan-sweep-test.mjs +0 -109
  371. package/scripts/set-effort-config-test.mjs +0 -41
  372. package/scripts/shell-failure-diagnostics-test.mjs +0 -280
  373. package/scripts/shell-hardening-test.mjs +0 -209
  374. package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
  375. package/scripts/ship-mode-test.mjs +0 -98
  376. package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
  377. package/scripts/smoke-loop-failure-summary.mjs +0 -16
  378. package/scripts/smoke-loop-report.mjs +0 -221
  379. package/scripts/smoke-loop.mjs +0 -202
  380. package/scripts/smoke-runtime-negative.ps1 +0 -106
  381. package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
  382. package/scripts/steering-drain-buckets-test.mjs +0 -467
  383. package/scripts/steering-fold-provenance-test.mjs +0 -71
  384. package/scripts/streaming-tail-window-test.mjs +0 -175
  385. package/scripts/submit-commandbusy-race-test.mjs +0 -257
  386. package/scripts/task-bench.mjs +0 -205
  387. package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
  388. package/scripts/termio-input-smoke.mjs +0 -199
  389. package/scripts/tool-result-hook-test.mjs +0 -48
  390. package/scripts/tool-smoke.mjs +0 -2453
  391. package/scripts/tool-tui-presentation-test.mjs +0 -286
  392. package/scripts/toolcall-args-test.mjs +0 -239
  393. package/scripts/tui-ambiguous-width-test.mjs +0 -113
  394. package/scripts/tui-background-failure-smoke.mjs +0 -73
  395. package/scripts/tui-perf-run.ps1 +0 -26
  396. package/scripts/tui-render-smoke.mjs +0 -90
  397. package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
  398. package/scripts/tui-runtime-load-bench.mjs +0 -50
  399. package/scripts/tui-store-frame-batch-test.mjs +0 -99
  400. package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
  401. package/scripts/tui-transcript-perf-test.mjs +0 -934
  402. package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
  403. package/scripts/verify-release-assets-test.mjs +0 -281
  404. package/scripts/web-fetch-routing-test.mjs +0 -158
  405. package/scripts/webhook-smoke.mjs +0 -201
  406. package/scripts/windows-hide-spawn-options-test.mjs +0 -19
  407. package/scripts/worker-notify-rejection-test.mjs +0 -51
  408. package/scripts/write-backpressure-test.mjs +0 -147
@@ -1,1448 +0,0 @@
1
- #!/usr/bin/env node
2
- import test from 'node:test';
3
- import assert from 'node:assert/strict';
4
-
5
- import {
6
- convertSchema,
7
- normalizeGeminiParts,
8
- parseGeminiThinkingParts,
9
- parseGeminiTextPartMetadata,
10
- parseToolCalls,
11
- toGeminiContents,
12
- } from '../src/runtime/agent/orchestrator/providers/gemini-schema.mjs';
13
- import {
14
- GEMINI_GLOBAL_CACHE_MIN_LIVE_MS,
15
- _geminiCachePrefixHash,
16
- _geminiCredentialFingerprint,
17
- _geminiGlobalCacheKey,
18
- _invalidateGeminiCachesForCredentialFingerprint,
19
- _getGeminiGlobalCache,
20
- _setGeminiGlobalCache,
21
- geminiGlobalCaches,
22
- } from '../src/runtime/agent/orchestrator/providers/gemini-cache.mjs';
23
- import {
24
- aggregateGeminiStreamChunks,
25
- consumeGeminiRestStreamResponse,
26
- consumeGeminiSdkStream,
27
- geminiChunkText,
28
- } from '../src/runtime/agent/orchestrator/providers/gemini-stream.mjs';
29
- import {
30
- GeminiProvider,
31
- fetchGeminiModelPages,
32
- } from '../src/runtime/agent/orchestrator/providers/gemini.mjs';
33
- import { _toAnthropicMessagesForTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
34
- import { classifyError } from '../src/runtime/agent/orchestrator/providers/retry-classifier.mjs';
35
- import {
36
- enrichModels,
37
- loadCatalog,
38
- loadModelsDevCatalog,
39
- } from '../src/runtime/agent/orchestrator/providers/model-catalog.mjs';
40
-
41
- const networkDeny = async (url) => {
42
- throw new Error(`unexpected network: ${url}`);
43
- };
44
- const hermeticConfig = (extra = {}) => ({
45
- apiKey: 'test-key',
46
- fetchFn: networkDeny,
47
- preconnectFn: () => {},
48
- ...extra,
49
- });
50
-
51
- function assertForbiddenSchemaKeywordsAbsent(value) {
52
- if (!value || typeof value !== 'object') return;
53
- for (const [key, nested] of Object.entries(value)) {
54
- assert.equal(['oneOf', 'allOf', 'not'].includes(key), false, `forbidden Gemini schema keyword: ${key}`);
55
- assertForbiddenSchemaKeywordsAbsent(nested);
56
- }
57
- }
58
-
59
- function assertRequiredSubsetOfProperties(value) {
60
- if (!value || typeof value !== 'object') return;
61
- if (Array.isArray(value.required)) {
62
- assert.equal(value.properties && typeof value.properties === 'object', true);
63
- for (const name of value.required) {
64
- assert.equal(Object.hasOwn(value.properties, name), true, `missing required property schema: ${name}`);
65
- }
66
- }
67
- for (const nested of Object.values(value)) assertRequiredSubsetOfProperties(nested);
68
- }
69
-
70
- function assertSchemaConflict(schema, messagePattern = /could not be represented safely/) {
71
- assert.equal(schema.type, 'string');
72
- assert.deepEqual(schema.enum, ['__mixdog_unrepresentable_schema_conjunction__']);
73
- assert.match(schema.description, messagePattern);
74
- }
75
-
76
- function assertNoFileData(value) {
77
- if (!value || typeof value !== 'object') return;
78
- for (const [key, nested] of Object.entries(value)) {
79
- assert.notEqual(key, 'fileData');
80
- assertNoFileData(nested);
81
- }
82
- }
83
-
84
- test('Gemini stream preserves thought metadata but never relays thought text', () => {
85
- const chunk = {
86
- candidates: [{
87
- content: {
88
- role: 'model',
89
- parts: [
90
- { text: 'private reasoning', thought: true, thoughtSignature: 'sig-r' },
91
- { text: 'visible answer' },
92
- ],
93
- },
94
- finishReason: 'STOP',
95
- }],
96
- };
97
- assert.equal(geminiChunkText(chunk), 'visible answer');
98
- const aggregated = aggregateGeminiStreamChunks([chunk]);
99
- assert.deepEqual(aggregated.candidates[0].content.parts[0], {
100
- text: 'private reasoning',
101
- thought: true,
102
- thoughtSignature: 'sig-r',
103
- });
104
- assert.deepEqual(parseGeminiThinkingParts(aggregated.candidates[0].content.parts), [{
105
- type: 'thinking',
106
- thinking: 'private reasoning',
107
- signature: 'sig-r',
108
- }]);
109
- });
110
-
111
- test('Gemini function-call thought signatures round-trip at Part level', () => {
112
- const contents = toGeminiContents([{
113
- role: 'assistant',
114
- content: '',
115
- toolCalls: [{
116
- id: 'call-1',
117
- name: 'read',
118
- arguments: { path: 'a' },
119
- thoughtSignature: 'opaque',
120
- }],
121
- }]);
122
- assert.deepEqual(contents[0].parts[0], {
123
- functionCall: { name: 'read', args: { path: 'a' }, id: 'call-1' },
124
- thoughtSignature: 'opaque',
125
- });
126
- });
127
-
128
- test('Gemini provider-scoped thought parts round-trip before text and function calls', () => {
129
- const contents = toGeminiContents([{
130
- role: 'assistant',
131
- content: 'answer',
132
- providerMetadata: { gemini: { thoughtParts: [{ text: 'reason', thoughtSignature: 'sig-r' }] } },
133
- toolCalls: [{ id: 'call-1', name: 'read', arguments: { path: 'a' }, thoughtSignature: 'sig-f' }],
134
- }]);
135
- assert.deepEqual(contents[0].parts, [
136
- { text: 'reason', thought: true, thoughtSignature: 'sig-r' },
137
- { text: 'answer' },
138
- { functionCall: { name: 'read', args: { path: 'a' }, id: 'call-1' }, thoughtSignature: 'sig-f' },
139
- ]);
140
- });
141
-
142
- test('Gemini preserves native call ids and replays the same id in function responses', () => {
143
- const [call] = parseToolCalls([{ functionCall: { id: 'server/id: EXACT', name: 'read', args: { path: 'a' } } }]);
144
- assert.equal(call.id, 'server/id: EXACT');
145
- const contents = toGeminiContents([
146
- { role: 'assistant', content: '', toolCalls: [call] },
147
- { role: 'tool', toolCallId: call.id, content: 'ok' },
148
- ]);
149
- assert.equal(contents[0].parts[0].functionCall.id, 'server/id: EXACT');
150
- assert.equal(contents[1].parts[0].functionResponse.id, 'server/id: EXACT');
151
- assert.equal(contents[1].parts[0].functionResponse.name, 'read');
152
- const [fallbackA] = parseToolCalls([{ functionCall: { name: 'read', args: { path: 'a' } } }]);
153
- const [fallbackB] = parseToolCalls([{ functionCall: { name: 'read', args: { path: 'a' } } }]);
154
- assert.equal(fallbackA.id, fallbackB.id);
155
- });
156
-
157
- test('Gemini rejects malformed or unsigned hidden metadata and ignores foreign thinking blocks', () => {
158
- const message = {
159
- role: 'assistant',
160
- content: 'visible',
161
- thinkingBlocks: [{ type: 'thinking', thinking: 'anthropic secret', signature: 'anthropic-sig' }],
162
- providerMetadata: {
163
- gemini: {
164
- thoughtParts: [
165
- { text: 'valid-looking', thoughtSignature: 'gemini-sig' },
166
- { text: 'unsigned injection' },
167
- ],
168
- },
169
- },
170
- };
171
- assert.deepEqual(toGeminiContents([message])[0].parts, [{ text: 'visible' }]);
172
- assert.equal(JSON.stringify(_toAnthropicMessagesForTest([message])).includes('valid-looking'), false);
173
- });
174
-
175
- test('Gemini ordinary signed text parts survive canonical history and remain provider-scoped', () => {
176
- const metadata = parseGeminiTextPartMetadata([
177
- { text: 'hello ', thoughtSignature: 'sig-text-1' },
178
- { text: 'world' },
179
- ]);
180
- const message = { role: 'assistant', content: 'hello world', providerMetadata: metadata };
181
- assert.deepEqual(toGeminiContents([message])[0].parts, [
182
- { text: 'hello ', thoughtSignature: 'sig-text-1' },
183
- { text: 'world' },
184
- ]);
185
- const persisted = JSON.parse(JSON.stringify(message));
186
- assert.deepEqual(toGeminiContents([persisted])[0].parts, [
187
- { text: 'hello ', thoughtSignature: 'sig-text-1' },
188
- { text: 'world' },
189
- ]);
190
- assert.equal(JSON.stringify(_toAnthropicMessagesForTest([message])).includes('sig-text-1'), false);
191
- });
192
-
193
- test('Gemini schema projects nullable unions, const, and invalid required names', () => {
194
- const schema = convertSchema({
195
- type: 'object',
196
- properties: {
197
- mode: { type: ['string', 'null'], const: 'fast' },
198
- },
199
- required: ['mode', 'missing'],
200
- });
201
- assert.deepEqual(schema.required, ['mode', 'missing']);
202
- assert.equal(schema.properties.mode.type, 'string');
203
- assert.equal(schema.properties.mode.nullable, true);
204
- assert.deepEqual(schema.properties.mode.enum, ['fast']);
205
- assert.equal('const' in schema.properties.mode, false);
206
- assertSchemaConflict(schema.properties.missing, /required property missing has no representable schema/);
207
- assertRequiredSubsetOfProperties(schema);
208
- });
209
-
210
- test('Gemini normalizes nullable arrays and numeric constraints before type-dependent validation', () => {
211
- const array = convertSchema({ type: ['array', 'null'] });
212
- assert.equal(array.type, 'array');
213
- assert.equal(array.nullable, true);
214
- assert.deepEqual(array.items, { type: 'string' });
215
-
216
- const numeric = convertSchema({
217
- type: ['integer', 'null'],
218
- enum: [1, 2],
219
- minimum: 1,
220
- maximum: 3,
221
- });
222
- assertSchemaConflict(numeric, /does not support integer enum conjunction/);
223
- });
224
-
225
- test('Gemini allOf conjunction merges representable constraints without broadening', () => {
226
- assert.deepEqual(convertSchema({
227
- allOf: [{ type: 'string', minLength: 2 }, { type: 'string', minLength: 5, maxLength: 9 }],
228
- }), { type: 'string', minLength: 5, maxLength: 9 });
229
- assert.deepEqual(convertSchema({
230
- type: 'number',
231
- minimum: 0,
232
- maximum: 100,
233
- allOf: [{ minimum: 10 }, { maximum: 20 }],
234
- }), { type: 'number', minimum: 10, maximum: 20 });
235
- assert.deepEqual(convertSchema({
236
- type: 'integer',
237
- allOf: [{ exclusiveMinimum: 2 }, { exclusiveMaximum: 8 }],
238
- }), { type: 'integer', minimum: 3, maximum: 7 });
239
- assert.deepEqual(convertSchema({
240
- type: 'object',
241
- properties: { nested: { type: 'object', properties: { value: { type: 'string', minLength: 2 } } } },
242
- allOf: [{
243
- properties: { nested: { properties: { value: { type: 'string', maxLength: 8 } } } },
244
- }],
245
- }).properties.nested.properties.value, {
246
- type: 'string',
247
- minLength: 2,
248
- maxLength: 8,
249
- });
250
- assert.deepEqual(convertSchema({
251
- type: 'string',
252
- enum: ['a', 'b', 'c'],
253
- allOf: [{ enum: ['b', 'c', 'd'] }],
254
- }).enum, ['b', 'c']);
255
- });
256
-
257
- test('Gemini allOf conflicts produce an explicit valid safe fallback', () => {
258
- for (const schema of [
259
- { allOf: [{ type: 'string' }, { type: 'number' }] },
260
- { type: 'string', allOf: [{ minLength: 10 }, { maxLength: 2 }] },
261
- { type: 'string', allOf: [{ enum: ['a'] }, { enum: ['b'] }] },
262
- { type: 'string', allOf: [{ pattern: '^a' }, { pattern: 'b$' }] },
263
- ]) {
264
- const converted = convertSchema(schema);
265
- assert.equal(converted.type, 'string');
266
- assert.deepEqual(converted.enum, ['__mixdog_unrepresentable_schema_conjunction__']);
267
- assert.match(converted.description, /could not be represented safely/);
268
- }
269
- });
270
-
271
- test('Gemini const/enum and nested numeric allOf conflicts never broaden', () => {
272
- assertSchemaConflict(convertSchema({
273
- type: 'string',
274
- const: 'a',
275
- enum: ['b'],
276
- }), /empty enum intersection/);
277
- const nested = convertSchema({
278
- type: 'object',
279
- properties: {
280
- value: {
281
- type: 'integer',
282
- allOf: [{ enum: [1] }, { minimum: 0 }],
283
- },
284
- },
285
- });
286
- assertSchemaConflict(nested.properties.value, /does not support integer enum conjunction/);
287
- });
288
-
289
- test('Gemini required-only root and allOf branches retain conservative property schemas', () => {
290
- const root = convertSchema({ type: 'object', required: ['b', 'b', null, 3] });
291
- assert.deepEqual(root.required, ['b']);
292
- assertSchemaConflict(root.properties.b, /required property b has no representable schema/);
293
-
294
- const conjunction = convertSchema({
295
- type: 'object',
296
- properties: { a: { type: 'string' } },
297
- required: ['a'],
298
- allOf: [{ required: ['b'] }],
299
- });
300
- assert.deepEqual(conjunction.required, ['a', 'b']);
301
- assertSchemaConflict(conjunction.properties.b, /required property b has no representable schema/);
302
- assertRequiredSubsetOfProperties({ root, conjunction });
303
- });
304
-
305
- test('Gemini dangerous property names remain own serialized data properties recursively', () => {
306
- const objectPrototype = Object.getPrototypeOf({});
307
- const objectPrototypeKeys = Object.getOwnPropertyNames(Object.prototype);
308
- const schema = convertSchema(JSON.parse(`{
309
- "type": "object",
310
- "properties": {
311
- "__proto__": { "type": "string" },
312
- "child": {
313
- "type": "object",
314
- "required": ["__proto__", "constructor", "prototype"]
315
- }
316
- },
317
- "required": ["__proto__", "prototype"],
318
- "allOf": [{ "required": ["constructor"] }]
319
- }`));
320
-
321
- for (const properties of [schema.properties, schema.properties.child.properties]) {
322
- assert.equal(Object.getPrototypeOf(properties), Object.prototype);
323
- for (const name of ['__proto__', 'constructor', 'prototype']) {
324
- assert.equal(Object.hasOwn(properties, name), true, `${name} must be an own schema property`);
325
- const descriptor = Object.getOwnPropertyDescriptor(properties, name);
326
- assert.equal(descriptor.enumerable, true);
327
- assert.equal(descriptor.writable, true);
328
- assert.equal(descriptor.configurable, true);
329
- }
330
- }
331
- assert.equal(Object.getPrototypeOf({}), objectPrototype);
332
- assert.deepEqual(Object.getOwnPropertyNames(Object.prototype), objectPrototypeKeys);
333
- const serialized = JSON.stringify(schema);
334
- for (const name of ['__proto__', 'constructor', 'prototype']) {
335
- assert.equal(serialized.includes(`"${name}"`), true);
336
- assert.equal(Object.hasOwn(JSON.parse(serialized).properties, name), true);
337
- }
338
- assertRequiredSubsetOfProperties(schema);
339
- });
340
-
341
- test('Gemini compound schemas use only supported anyOf and preserve root fields', () => {
342
- const properties = {
343
- pattern: { type: 'string' },
344
- path: { type: 'string' },
345
- options: {
346
- type: 'object',
347
- properties: { hidden: { type: 'boolean' } },
348
- not: { required: ['hidden'] },
349
- },
350
- };
351
- const schema = convertSchema({
352
- type: 'object',
353
- properties,
354
- required: ['options'],
355
- oneOf: [{ required: ['pattern'] }, { required: ['path'] }],
356
- allOf: [{ properties: { limit: { type: 'integer' } }, required: ['limit'] }],
357
- });
358
- assert.deepEqual(Object.keys(schema.properties).sort(), ['limit', 'options', 'path', 'pattern']);
359
- assert.deepEqual(schema.required.sort(), ['limit', 'options']);
360
- assert.equal(Array.isArray(schema.anyOf), true);
361
- assert.deepEqual(schema.anyOf.map((branch) => branch.required), [['pattern'], ['path']]);
362
- assertForbiddenSchemaKeywordsAbsent(schema);
363
- assertRequiredSubsetOfProperties(schema);
364
- });
365
-
366
- test('Gemini schema projection drops unknown JSON Schema and extension keywords recursively', () => {
367
- const schema = convertSchema({
368
- type: 'object',
369
- additionalProperties: false,
370
- unevaluatedProperties: false,
371
- 'x-custom': true,
372
- properties: {
373
- value: {
374
- type: 'string',
375
- description: 'kept',
376
- examples: ['drop'],
377
- contentEncoding: 'base64',
378
- },
379
- },
380
- required: ['value'],
381
- });
382
- assert.deepEqual(schema, {
383
- type: 'object',
384
- properties: { value: { type: 'string', description: 'kept' } },
385
- required: ['value'],
386
- });
387
- });
388
-
389
- test('Gemini function-response media is nested, referenced, and MIME-safe', () => {
390
- assert.deepEqual(normalizeGeminiParts([
391
- { inlineData: { mimeType: 'audio/wav', data: 'YXVkaW8=' } },
392
- { fileData: { mimeType: 'video/mp4', fileUri: 'https://files.example/video' } },
393
- ]), [
394
- { inlineData: { mimeType: 'audio/wav', data: 'YXVkaW8=' } },
395
- { fileData: { mimeType: 'video/mp4', fileUri: 'https://files.example/video' } },
396
- ]);
397
- const tool = toGeminiContents([{
398
- role: 'tool',
399
- toolCallId: 'inspect_media',
400
- content: [
401
- { text: 'clip' },
402
- { inlineData: { mimeType: 'image/png', data: 'aW1hZ2U=' } },
403
- { fileData: { mimeType: 'application/pdf', fileUri: 'files/report' } },
404
- { inlineData: { mimeType: 'audio/wav', data: 'YXVkaW8=' } },
405
- ],
406
- }]);
407
- assert.deepEqual(tool[0], {
408
- role: 'user',
409
- parts: [{
410
- functionResponse: {
411
- name: 'inspect_media',
412
- id: 'inspect_media',
413
- response: {
414
- result: 'clip',
415
- media: [{ $ref: 'tool_media_1' }],
416
- externalMedia: [{ mimeType: 'application/pdf', fileUri: 'files/report' }],
417
- omittedMediaTypes: ['audio/wav'],
418
- },
419
- parts: [
420
- { inlineData: { mimeType: 'image/png', data: 'aW1hZ2U=', displayName: 'tool_media_1' } },
421
- ],
422
- },
423
- }],
424
- });
425
- assert.equal(tool[0].parts.length, 1, 'media must not escape functionResponse as sibling Content parts');
426
- assertNoFileData(tool[0].parts[0].functionResponse);
427
- });
428
-
429
- test('Gemini global cache accepts a fresh default-five-minute entry', () => {
430
- assert.equal(GEMINI_GLOBAL_CACHE_MIN_LIVE_MS, 75_000);
431
- geminiGlobalCaches.clear();
432
- const now = Date.now();
433
- _setGeminiGlobalCache('k', {
434
- cacheName: 'cachedContents/fresh',
435
- cacheExpiresAt: now + 5 * 60_000,
436
- });
437
- assert.equal(_getGeminiGlobalCache('k', now)?.cacheName, 'cachedContents/fresh');
438
- });
439
-
440
- test('Gemini global cache rejects entries inside first-byte headroom', () => {
441
- geminiGlobalCaches.clear();
442
- const now = Date.now();
443
- _setGeminiGlobalCache('near-expiry', {
444
- cacheName: 'cachedContents/near-expiry',
445
- cacheExpiresAt: now + 70_000,
446
- });
447
- assert.equal(_getGeminiGlobalCache('near-expiry', now), null);
448
- });
449
-
450
- test('Gemini cache identity includes toolConfig', () => {
451
- const base = {
452
- model: 'gemini-2.5-flash',
453
- systemInstruction: 'system',
454
- geminiTools: [{ functionDeclarations: [{ name: 'read' }] }],
455
- contents: [{ role: 'user', parts: [{ text: 'x' }] }],
456
- prefixCount: 0,
457
- };
458
- const auto = _geminiCachePrefixHash({
459
- ...base,
460
- toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
461
- });
462
- const none = _geminiCachePrefixHash({
463
- ...base,
464
- toolConfig: { functionCallingConfig: { mode: 'NONE' } },
465
- });
466
- assert.notEqual(auto, none);
467
- });
468
-
469
- test('Gemini cache identity is credential-bound and invalidatable without raw keys', () => {
470
- geminiGlobalCaches.clear();
471
- const rawKey = 'super-secret-key';
472
- const fingerprint = _geminiCredentialFingerprint(rawKey);
473
- assert.notEqual(fingerprint, rawKey);
474
- assert.equal(fingerprint.includes(rawKey), false);
475
- const key = _geminiGlobalCacheKey({
476
- credentialFingerprint: fingerprint,
477
- model: 'gemini-2.5-flash',
478
- cachePrefixHash: 'prefix',
479
- cachePrefixContentCount: 1,
480
- });
481
- _setGeminiGlobalCache(key, {
482
- cacheName: 'cachedContents/credential',
483
- cacheExpiresAt: Date.now() + 60_000,
484
- cacheCredentialFingerprint: fingerprint,
485
- });
486
- assert.equal(_invalidateGeminiCachesForCredentialFingerprint(fingerprint), 1);
487
- assert.equal(geminiGlobalCaches.size, 0);
488
- assert.equal(JSON.stringify([...geminiGlobalCaches]).includes(rawKey), false);
489
- });
490
-
491
- test('Gemini cachedContents creation carries toolConfig with the tool schema', async () => {
492
- geminiGlobalCaches.clear();
493
- let injectedRequests = 0;
494
- let capturedBody;
495
- const provider = new GeminiProvider(hermeticConfig({ fetchFn: async (_url, init) => {
496
- injectedRequests += 1;
497
- capturedBody = JSON.parse(init.body);
498
- return {
499
- ok: true,
500
- async json() {
501
- return {
502
- name: 'cachedContents/tool-choice',
503
- usageMetadata: { totalTokenCount: 3000 },
504
- };
505
- },
506
- };
507
- } }));
508
- try {
509
- const toolConfig = { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: ['read'] } };
510
- const name = await provider._ensureGeminiCache({
511
- apiKey: 'test-key',
512
- model: 'gemini-2.5-flash',
513
- systemInstruction: 'x'.repeat(9000),
514
- geminiTools: [{ functionDeclarations: [{ name: 'read', parameters: { type: 'object' } }] }],
515
- toolConfig,
516
- contents: [
517
- { role: 'user', parts: [{ text: 'prefix' }] },
518
- { role: 'user', parts: [{ text: 'latest' }] },
519
- ],
520
- opts: { providerState: {}, iteration: 1 },
521
- });
522
- assert.equal(name, 'cachedContents/tool-choice');
523
- assert.deepEqual(capturedBody.toolConfig, toolConfig);
524
- assert.equal(capturedBody.tools[0].functionDeclarations[0].name, 'read');
525
- assert.equal(injectedRequests, 1);
526
- } finally {
527
- geminiGlobalCaches.clear();
528
- }
529
- });
530
-
531
- test('Gemini cachedContents creation retries transient failures within policy', async () => {
532
- geminiGlobalCaches.clear();
533
- let calls = 0;
534
- const provider = new GeminiProvider(hermeticConfig({ fetchFn: async () => {
535
- calls += 1;
536
- if (calls === 1) {
537
- return {
538
- ok: false,
539
- status: 503,
540
- headers: new Headers({ 'Retry-After': '0' }),
541
- async text() { return '{"error":{"message":"temporarily unavailable"}}'; },
542
- };
543
- }
544
- return {
545
- ok: true,
546
- async json() {
547
- return {
548
- name: 'cachedContents/retried',
549
- usageMetadata: { totalTokenCount: 3000 },
550
- };
551
- },
552
- };
553
- } }));
554
- try {
555
- const name = await provider._ensureGeminiCache({
556
- apiKey: 'test-key',
557
- model: 'gemini-2.5-flash',
558
- systemInstruction: 'x'.repeat(9000),
559
- contents: [
560
- { role: 'user', parts: [{ text: 'prefix' }] },
561
- { role: 'user', parts: [{ text: 'latest' }] },
562
- ],
563
- opts: { providerState: {}, iteration: 1 },
564
- });
565
- assert.equal(name, 'cachedContents/retried');
566
- assert.equal(calls, 2);
567
- } finally {
568
- geminiGlobalCaches.clear();
569
- }
570
- });
571
-
572
- test('Gemini constructor and send are network-hermetic through injected transports', async () => {
573
- let preconnectCalls = 0;
574
- let fetchCalls = 0;
575
- const provider = new GeminiProvider(hermeticConfig({
576
- preconnectFn: () => { preconnectCalls += 1; },
577
- fetchFn: async (url) => {
578
- fetchCalls += 1;
579
- return networkDeny(url);
580
- },
581
- genAI: {
582
- getGenerativeModel() {
583
- return {
584
- async generateContentStream() {
585
- return sdkStream([{
586
- candidates: [{ content: { role: 'model', parts: [{ text: 'ok' }] }, finishReason: 'STOP' }],
587
- }]);
588
- },
589
- };
590
- },
591
- },
592
- }));
593
- provider._ensureGeminiCache = async () => null;
594
- const result = await provider.send([{ role: 'user', content: 'x' }], 'gemini-2.5-flash', [], {});
595
- assert.equal(result.content, 'ok');
596
- assert.equal(preconnectCalls, 2);
597
- assert.equal(fetchCalls, 0);
598
- });
599
-
600
- test('Gemini model listing follows nextPageToken deterministically', async () => {
601
- const urls = [];
602
- const pages = [
603
- { models: [{ name: 'models/gemini-a' }], nextPageToken: 'next token' },
604
- { models: [{ name: 'models/gemini-b' }] },
605
- ];
606
- const items = await fetchGeminiModelPages('key value', async (url) => {
607
- urls.push(url);
608
- return {
609
- ok: true,
610
- async json() { return pages.shift(); },
611
- };
612
- });
613
- assert.deepEqual(items.map((item) => item.name), ['models/gemini-a', 'models/gemini-b']);
614
- assert.match(urls[0], /pageSize=1000/);
615
- assert.match(urls[1], /pageToken=next\+token/);
616
- assert.equal(urls.every((url) => url.includes('key=key+value')), true);
617
- });
618
-
619
- test('Gemini listModels routes native and enrichment catalog fetches through the injected transport', async () => {
620
- const urls = [];
621
- let saved = null;
622
- const originalGlobalFetch = globalThis.fetch;
623
- let globalFetchCalls = 0;
624
- globalThis.fetch = async () => {
625
- globalFetchCalls += 1;
626
- throw new Error('global fetch must not run');
627
- };
628
- try {
629
- const provider = new GeminiProvider(hermeticConfig({
630
- catalogForceRefresh: true,
631
- modelCache: {
632
- loadSync: () => null,
633
- save: (models) => { saved = models; },
634
- },
635
- fetchFn: async (url) => {
636
- urls.push(String(url));
637
- if (String(url).includes('generativelanguage.googleapis.com')) {
638
- return {
639
- ok: true,
640
- async json() {
641
- return {
642
- models: [{
643
- name: 'models/gemini-hermetic',
644
- displayName: 'Hermetic',
645
- supportedGenerationMethods: ['generateContent'],
646
- }],
647
- };
648
- },
649
- };
650
- }
651
- if (String(url).includes('model_prices_and_context_window.json')) {
652
- return {
653
- ok: true,
654
- async json() {
655
- return {
656
- 'gemini/gemini-hermetic': {
657
- litellm_provider: 'gemini',
658
- input_cost_per_token: 1e-6,
659
- },
660
- };
661
- },
662
- };
663
- }
664
- if (String(url).includes('models.dev/api.json')) {
665
- return { ok: true, async json() { return { google: { models: {} } }; } };
666
- }
667
- throw new Error(`unexpected injected URL: ${url}`);
668
- },
669
- }));
670
- const models = await provider.listModels();
671
- assert.deepEqual(models.map((model) => model.id), ['gemini-hermetic']);
672
- assert.deepEqual(saved, models);
673
- assert.equal(urls.some((url) => url.includes('generativelanguage.googleapis.com')), true);
674
- assert.equal(urls.some((url) => url.includes('model_prices_and_context_window.json')), true);
675
- assert.equal(urls.some((url) => url.includes('models.dev/api.json')), true);
676
- assert.equal(globalFetchCalls, 0);
677
- } finally {
678
- globalThis.fetch = originalGlobalFetch;
679
- }
680
- });
681
-
682
- test('injected models.dev results are request-local and used exactly for enrichment', async () => {
683
- const injected = {
684
- google: {
685
- models: {
686
- 'gemini-injected-only': {
687
- cost: { input: 7, output: 11 },
688
- limit: { context: 12345, output: 678 },
689
- },
690
- },
691
- },
692
- };
693
- const originalFetch = globalThis.fetch;
694
- globalThis.fetch = async () => ({
695
- ok: true,
696
- async json() { return { global: { models: {} } }; },
697
- });
698
- try {
699
- await loadModelsDevCatalog({ force: true });
700
- const models = await enrichModels(
701
- [{ id: 'gemini-injected-only', provider: 'gemini' }],
702
- {
703
- fetchFn: async (url) => ({
704
- ok: true,
705
- async json() {
706
- return String(url).includes('models.dev/api.json') ? injected : {};
707
- },
708
- }),
709
- },
710
- );
711
- assert.equal(models[0].inputCostPerM, 7);
712
- assert.equal(models[0].outputCostPerM, 11);
713
- assert.equal(models[0].contextWindow, 12345);
714
- assert.equal(models[0].outputTokens, 678);
715
- } finally {
716
- globalThis.fetch = originalFetch;
717
- }
718
- });
719
-
720
- test('injected LiteLLM results are request-local and used exactly for enrichment', async () => {
721
- const originalFetch = globalThis.fetch;
722
- globalThis.fetch = async () => ({
723
- ok: true,
724
- async json() { return { 'global-only': { input_cost_per_token: 1e-6 } }; },
725
- });
726
- try {
727
- await loadCatalog({ force: true });
728
- const models = await enrichModels([{ id: 'litellm-injected-only' }], {
729
- fetchFn: async (url) => {
730
- if (!String(url).includes('model_prices_and_context_window.json')) {
731
- throw new Error(`unexpected injected URL: ${url}`);
732
- }
733
- return {
734
- ok: true,
735
- async json() {
736
- return {
737
- 'litellm-injected-only': {
738
- input_cost_per_token: 7e-6,
739
- output_cost_per_token: 11e-6,
740
- max_input_tokens: 12345,
741
- max_output_tokens: 678,
742
- },
743
- };
744
- },
745
- };
746
- },
747
- });
748
- assert.equal(models[0].inputCostPerM, 7);
749
- assert.equal(models[0].outputCostPerM, 11);
750
- assert.equal(models[0].contextWindow, 12345);
751
- assert.equal(models[0].outputTokens, 678);
752
- } finally {
753
- globalThis.fetch = originalFetch;
754
- }
755
- });
756
-
757
- test('injected LiteLLM failure does not prevent a subsequent normal refresh', async () => {
758
- assert.deepEqual(await loadCatalog({
759
- force: true,
760
- fetchFn: async () => { throw new Error('injected failure'); },
761
- }), {});
762
- const originalFetch = globalThis.fetch;
763
- let normalCalls = 0;
764
- globalThis.fetch = async () => {
765
- normalCalls += 1;
766
- return { ok: true, async json() { return { normal: {} }; } };
767
- };
768
- try {
769
- assert.deepEqual(await loadCatalog({ force: true }), { normal: {} });
770
- assert.equal(normalCalls, 1);
771
- } finally {
772
- globalThis.fetch = originalFetch;
773
- }
774
- });
775
-
776
- test('injected LiteLLM load neither joins nor overwrites a concurrent global singleflight', async () => {
777
- const originalFetch = globalThis.fetch;
778
- let resolveGlobal;
779
- let resolveInjected;
780
- let globalCalls = 0;
781
- globalThis.fetch = async () => {
782
- globalCalls += 1;
783
- return new Promise((resolve) => { resolveGlobal = resolve; });
784
- };
785
- try {
786
- const globalLoad = loadCatalog({ force: true });
787
- await Promise.resolve();
788
- const localLoad = loadCatalog({
789
- force: true,
790
- fetchFn: async () => new Promise((resolve) => { resolveInjected = resolve; }),
791
- });
792
- assert.equal(globalCalls, 1);
793
- resolveGlobal({ ok: true, async json() { return { global: {} }; } });
794
- assert.deepEqual(await globalLoad, { global: {} });
795
- resolveInjected({ ok: true, async json() { return { injected: {} }; } });
796
- assert.deepEqual(await localLoad, { injected: {} });
797
- assert.deepEqual(await loadCatalog(), { global: {} });
798
- assert.equal(globalCalls, 1);
799
- } finally {
800
- globalThis.fetch = originalFetch;
801
- }
802
- });
803
-
804
- test('injected models.dev failure does not prevent a subsequent normal refresh', async () => {
805
- const failed = await loadModelsDevCatalog({
806
- force: true,
807
- fetchFn: async () => { throw new Error('injected failure'); },
808
- });
809
- assert.deepEqual(failed, {});
810
-
811
- const originalFetch = globalThis.fetch;
812
- let normalCalls = 0;
813
- globalThis.fetch = async () => {
814
- normalCalls += 1;
815
- return { ok: true, async json() { return { normal: { models: {} } }; } };
816
- };
817
- try {
818
- const normal = await loadModelsDevCatalog({ force: true });
819
- assert.deepEqual(normal, { normal: { models: {} } });
820
- assert.equal(normalCalls, 1);
821
- } finally {
822
- globalThis.fetch = originalFetch;
823
- }
824
- });
825
-
826
- test('injected models.dev load neither joins nor overwrites a concurrent global singleflight', async () => {
827
- const originalFetch = globalThis.fetch;
828
- let resolveGlobal;
829
- let resolveInjected;
830
- let globalCalls = 0;
831
- globalThis.fetch = async () => {
832
- globalCalls += 1;
833
- return new Promise((resolve) => { resolveGlobal = resolve; });
834
- };
835
- try {
836
- const globalLoad = loadModelsDevCatalog({ force: true });
837
- await Promise.resolve();
838
- const localLoad = loadModelsDevCatalog({
839
- force: true,
840
- fetchFn: async () => new Promise((resolve) => { resolveInjected = resolve; }),
841
- });
842
- assert.equal(globalCalls, 1);
843
- resolveGlobal({
844
- ok: true,
845
- async json() { return { global: { models: {} } }; },
846
- });
847
- assert.deepEqual(await globalLoad, { global: { models: {} } });
848
- resolveInjected({
849
- ok: true,
850
- async json() { return { injected: { models: {} } }; },
851
- });
852
- assert.deepEqual(await localLoad, { injected: { models: {} } });
853
- assert.deepEqual(await loadModelsDevCatalog(), { global: { models: {} } });
854
- assert.equal(globalCalls, 1);
855
- } finally {
856
- globalThis.fetch = originalFetch;
857
- }
858
- });
859
-
860
- function sdkStream(chunks) {
861
- return {
862
- stream: {
863
- async *[Symbol.asyncIterator]() {
864
- for (const chunk of chunks) yield chunk;
865
- },
866
- },
867
- };
868
- }
869
-
870
- function finalizingToolGuard() {
871
- let finalized = false;
872
- return {
873
- feedText() {},
874
- finalize() { finalized = true; },
875
- getVisibleText() { return ''; },
876
- getLeakedToolCalls() {
877
- return finalized ? [{ id: 'gemini_leaked_final', name: 'read', arguments: {} }] : [];
878
- },
879
- };
880
- }
881
-
882
- test('Gemini REST EOF finalizes buffered leaked tools before classifying truncation', async () => {
883
- const body = new ReadableStream({
884
- start(controller) {
885
- controller.enqueue(new TextEncoder().encode(
886
- 'data: {"candidates":[{"content":{"parts":[{"text":"<invoke"}]}}]}\n',
887
- ));
888
- controller.close();
889
- },
890
- });
891
- const error = await consumeGeminiRestStreamResponse(
892
- { body },
893
- { label: 'REST EOF leak', textLeakGuard: finalizingToolGuard() },
894
- ).then(() => null, (caught) => caught);
895
- assert.equal(error.code, 'TRUNCATED_STREAM');
896
- assert.equal(error.emittedToolCall, true);
897
- assert.equal(error.unsafeToRetry, true);
898
- assert.equal(error.streamStalled, undefined);
899
- assert.equal(error.partialContent, undefined);
900
- assert.equal(classifyError(error), 'permanent');
901
- });
902
-
903
- test('Gemini REST malformed SSE is retryable corruption even before a later STOP', async () => {
904
- const body = new ReadableStream({
905
- start(controller) {
906
- controller.enqueue(new TextEncoder().encode([
907
- 'data: {not-json}',
908
- 'data: {"candidates":[{"finishReason":"STOP"}]}',
909
- '',
910
- ].join('\n')));
911
- controller.close();
912
- },
913
- });
914
- const error = await consumeGeminiRestStreamResponse(
915
- { body },
916
- { label: 'REST corrupt SSE' },
917
- ).then(() => null, (caught) => caught);
918
- assert.equal(error.streamCorruption, true);
919
- assert.equal(error.code, 'TRUNCATED_STREAM');
920
- assert.equal(classifyError(error), 'transient');
921
- });
922
-
923
- test('Gemini REST already-aborted signal does not lock the response body', async () => {
924
- const controller = new AbortController();
925
- controller.abort(new Error('already closed'));
926
- const body = new ReadableStream({ start() {} });
927
- await assert.rejects(
928
- consumeGeminiRestStreamResponse(
929
- { body },
930
- { label: 'REST pre-abort', signal: controller.signal },
931
- ),
932
- /already closed/,
933
- );
934
- assert.equal(body.locked, false);
935
- });
936
-
937
- test('Gemini SDK first-byte timeout cancels generation before rejecting', async () => {
938
- let cancelled = false;
939
- let returned = false;
940
- const streamResult = {
941
- stream: {
942
- [Symbol.asyncIterator]() {
943
- return {
944
- next() { return new Promise(() => {}); },
945
- async return() {
946
- await new Promise((resolve) => setTimeout(resolve, 10));
947
- returned = true;
948
- return { done: true };
949
- },
950
- };
951
- },
952
- },
953
- };
954
- const error = await consumeGeminiSdkStream(streamResult, {
955
- label: 'SDK timeout cancellation',
956
- firstByteTimeoutMs: 5,
957
- cancelGeneration: () => { cancelled = true; },
958
- }).then(() => null, (caught) => caught);
959
- assert.equal(error.code, 'EGEMINITIMEOUT');
960
- assert.equal(cancelled, true);
961
- assert.equal(returned, true);
962
- });
963
-
964
- test('Gemini SDK timeout is bounded when iterator return never settles', async () => {
965
- let cancelled = false;
966
- const startedAt = Date.now();
967
- const streamResult = {
968
- stream: {
969
- [Symbol.asyncIterator]() {
970
- return {
971
- next() { return new Promise(() => {}); },
972
- return() { return new Promise(() => {}); },
973
- };
974
- },
975
- },
976
- response: new Promise(() => {}),
977
- };
978
- const error = await consumeGeminiSdkStream(streamResult, {
979
- label: 'SDK stuck cancellation',
980
- firstByteTimeoutMs: 5,
981
- cancellationGraceMs: 20,
982
- cancelGeneration: () => { cancelled = true; },
983
- }).then(() => null, (caught) => caught);
984
- assert.equal(error.code, 'EGEMINITIMEOUT');
985
- assert.equal(cancelled, true);
986
- assert.ok(Date.now() - startedAt < 200, 'stuck iterator.return must not deadlock timeout rejection');
987
- });
988
-
989
- test('Gemini SDK parser errors are retryable stream corruption', async () => {
990
- const parseFailure = Object.assign(
991
- new Error('[GoogleGenerativeAI Error]: Error parsing JSON response: Unexpected end of JSON input'),
992
- { name: 'GoogleGenerativeAIError' },
993
- );
994
- const streamResult = {
995
- stream: {
996
- [Symbol.asyncIterator]() {
997
- return {
998
- async next() { throw parseFailure; },
999
- async return() { return { done: true }; },
1000
- };
1001
- },
1002
- },
1003
- response: Promise.reject(parseFailure),
1004
- };
1005
- const error = await consumeGeminiSdkStream(streamResult, {
1006
- label: 'SDK corrupt SSE',
1007
- }).then(() => null, (caught) => caught);
1008
- assert.equal(error.streamCorruption, true);
1009
- assert.equal(error.code, 'TRUNCATED_STREAM');
1010
- assert.equal(classifyError(error), 'transient');
1011
- });
1012
-
1013
- test('Gemini native function call is retry-safe until provider dispatch', async () => {
1014
- const streamResult = {
1015
- stream: {
1016
- async *[Symbol.asyncIterator]() {
1017
- yield { candidates: [{ content: { parts: [{ functionCall: { name: 'read', args: {} } }] } }] };
1018
- throw Object.assign(new Error('socket reset'), { code: 'ECONNRESET' });
1019
- },
1020
- },
1021
- };
1022
- const error = await consumeGeminiSdkStream(streamResult, {
1023
- label: 'SDK pre-dispatch function call',
1024
- }).then(() => null, (caught) => caught);
1025
- assert.equal(error.emittedToolCall, undefined);
1026
- assert.equal(error.unsafeToRetry, undefined);
1027
- assert.equal(classifyError(error), 'transient');
1028
- });
1029
-
1030
- test('Gemini SDK failure finalizes buffered leaked tools before retry-safety stamps', async () => {
1031
- const streamResult = {
1032
- stream: {
1033
- async *[Symbol.asyncIterator]() {
1034
- yield { candidates: [{ content: { parts: [{ text: '<invoke' }] } }] };
1035
- throw Object.assign(new Error('socket reset'), { code: 'ECONNRESET' });
1036
- },
1037
- },
1038
- };
1039
- const error = await consumeGeminiSdkStream(streamResult, {
1040
- label: 'SDK stalled leak',
1041
- textLeakGuard: finalizingToolGuard(),
1042
- }).then(() => null, (caught) => caught);
1043
- assert.equal(error.emittedToolCall, true);
1044
- assert.equal(error.unsafeToRetry, true);
1045
- assert.equal(error.streamStalled, undefined);
1046
- assert.equal(error.partialContent, undefined);
1047
- assert.equal(classifyError(error), 'permanent');
1048
- });
1049
-
1050
- function providerForChunks(chunks) {
1051
- const provider = new GeminiProvider(hermeticConfig());
1052
- provider._ensureGeminiCache = async () => null;
1053
- provider.genAI = {
1054
- getGenerativeModel() {
1055
- return {
1056
- async generateContentStream() {
1057
- return sdkStream(chunks);
1058
- },
1059
- };
1060
- },
1061
- };
1062
- return provider;
1063
- }
1064
-
1065
- test('Gemini provider excludes thought text and returns provider-scoped replay metadata', async () => {
1066
- const provider = providerForChunks([{
1067
- candidates: [{
1068
- content: {
1069
- role: 'model',
1070
- parts: [
1071
- { text: 'reason', thought: true, thoughtSignature: 'sig' },
1072
- { text: 'answer' },
1073
- ],
1074
- },
1075
- finishReason: 'STOP',
1076
- }],
1077
- usageMetadata: {
1078
- promptTokenCount: 10,
1079
- candidatesTokenCount: 2,
1080
- thoughtsTokenCount: 3,
1081
- },
1082
- }]);
1083
- const deltas = [];
1084
- const result = await provider.send(
1085
- [{ role: 'user', content: 'hello' }],
1086
- 'gemini-2.5-flash',
1087
- [],
1088
- { onTextDelta: (text) => deltas.push(text) },
1089
- );
1090
- assert.equal(result.content, 'answer');
1091
- assert.equal(deltas.join(''), 'answer');
1092
- assert.deepEqual(result.providerMetadata, {
1093
- gemini: {
1094
- thoughtParts: [{ text: 'reason', thoughtSignature: 'sig' }],
1095
- },
1096
- });
1097
- assert.equal(result.usage.outputTokens, 5);
1098
- });
1099
-
1100
- test('Gemini MAX_TOKENS error retains signed provider metadata for continuation', async () => {
1101
- const provider = providerForChunks([{
1102
- candidates: [{
1103
- content: {
1104
- role: 'model',
1105
- parts: [
1106
- { text: 'private', thought: true, thoughtSignature: 'sig-private' },
1107
- { text: 'partial' },
1108
- ],
1109
- },
1110
- finishReason: 'MAX_TOKENS',
1111
- }],
1112
- }]);
1113
- const error = await provider.send(
1114
- [{ role: 'user', content: 'hello' }],
1115
- 'gemini-2.5-flash',
1116
- [],
1117
- {},
1118
- ).then(() => null, (caught) => caught);
1119
- assert.deepEqual(error.providerMetadata, {
1120
- gemini: { thoughtParts: [{ text: 'private', thoughtSignature: 'sig-private' }] },
1121
- });
1122
- });
1123
-
1124
- test('Gemini stream truncation after signed thought/text attaches validated replay metadata', async () => {
1125
- const streamResult = {
1126
- stream: {
1127
- [Symbol.asyncIterator]() {
1128
- let emitted = false;
1129
- return {
1130
- async next() {
1131
- if (emitted) throw Object.assign(new Error('stream truncated'), { code: 'TRUNCATED_STREAM', truncatedStream: true });
1132
- emitted = true;
1133
- return {
1134
- done: false,
1135
- value: {
1136
- candidates: [{
1137
- content: {
1138
- role: 'model',
1139
- parts: [
1140
- { text: 'private', thought: true, thoughtSignature: 'sig-private' },
1141
- { text: 'partial', thoughtSignature: 'sig-text' },
1142
- ],
1143
- },
1144
- }],
1145
- },
1146
- };
1147
- },
1148
- async return() { return { done: true }; },
1149
- };
1150
- },
1151
- },
1152
- };
1153
- const error = await consumeGeminiSdkStream(streamResult, {
1154
- onTextDelta: () => {},
1155
- label: 'test Gemini SDK stream',
1156
- }).then(() => null, (caught) => caught);
1157
- assert.equal(error.partialContent, 'partial');
1158
- assert.deepEqual(error.providerMetadata, {
1159
- gemini: {
1160
- thoughtParts: [{ text: 'private', thoughtSignature: 'sig-private' }],
1161
- textParts: [{ text: 'partial', thoughtSignature: 'sig-text' }],
1162
- },
1163
- });
1164
- });
1165
-
1166
- for (const finishReason of [
1167
- 'MAX_TOKENS',
1168
- 'SAFETY',
1169
- 'IMAGE_SAFETY',
1170
- 'UNEXPECTED_TOOL_CALL',
1171
- 'TOO_MANY_TOOL_CALLS',
1172
- 'MALFORMED_RESPONSE',
1173
- ]) {
1174
- test(`Gemini ${finishReason} is provider-incomplete`, async () => {
1175
- const provider = providerForChunks([{
1176
- candidates: [{
1177
- content: { role: 'model', parts: [{ text: 'partial' }] },
1178
- finishReason,
1179
- }],
1180
- }]);
1181
- const error = await provider.send(
1182
- [{ role: 'user', content: 'hello' }],
1183
- 'gemini-2.5-flash',
1184
- [],
1185
- {},
1186
- ).then(() => null, (caught) => caught);
1187
- assert.equal(error?.code, 'PROVIDER_INCOMPLETE');
1188
- assert.equal(error?.finishReason, finishReason);
1189
- assert.equal(error?.partialContent, 'partial');
1190
- });
1191
- }
1192
-
1193
- test('Gemini prompt safety block is incomplete instead of a truncated stream', async () => {
1194
- const provider = providerForChunks([{
1195
- promptFeedback: { blockReason: 'SAFETY' },
1196
- }]);
1197
- const error = await provider.send(
1198
- [{ role: 'user', content: 'hello' }],
1199
- 'gemini-2.5-flash',
1200
- [],
1201
- {},
1202
- ).then(() => null, (caught) => caught);
1203
- assert.equal(error?.code, 'PROVIDER_INCOMPLETE');
1204
- assert.equal(error?.finishReason, 'PROMPT_SAFETY');
1205
- });
1206
-
1207
- test('Gemini auth reload retries a status-coded 401 exactly once', async () => {
1208
- const oldFingerprint = _geminiCredentialFingerprint('old');
1209
- const provider = new GeminiProvider(hermeticConfig({ apiKey: 'old' }));
1210
- let sends = 0;
1211
- let reloads = 0;
1212
- provider._doSend = async () => {
1213
- sends += 1;
1214
- if (sends === 1) throw Object.assign(new Error('unauthorized'), { status: 401 });
1215
- return { content: 'ok' };
1216
- };
1217
- provider.reloadApiKey = () => { reloads += 1; return 'replacement'; };
1218
- const opts = {
1219
- providerState: {
1220
- gemini: {
1221
- cacheName: 'cachedContents/old',
1222
- cacheCredentialFingerprint: oldFingerprint,
1223
- },
1224
- },
1225
- };
1226
- const result = await provider.send([], 'gemini-2.5-flash', [], opts);
1227
- assert.equal(result.content, 'ok');
1228
- assert.equal(sends, 2);
1229
- assert.equal(reloads, 1);
1230
- assert.equal(opts.providerState.gemini, undefined);
1231
- });
1232
-
1233
- test('Gemini evicted cachedContent is invalidated and retried uncached', async () => {
1234
- geminiGlobalCaches.clear();
1235
- _setGeminiGlobalCache('evicted-key', {
1236
- cacheName: 'cachedContents/evicted',
1237
- cacheExpiresAt: Date.now() + 5 * 60_000,
1238
- });
1239
- let cacheChecks = 0;
1240
- let restCalls = 0;
1241
- let sdkCalls = 0;
1242
- const provider = new GeminiProvider(hermeticConfig({
1243
- fetchFn: async () => {
1244
- restCalls += 1;
1245
- return {
1246
- ok: false,
1247
- status: 404,
1248
- headers: new Headers(),
1249
- async text() {
1250
- return JSON.stringify({ error: { message: 'Cached content not found', status: 'NOT_FOUND' } });
1251
- },
1252
- };
1253
- },
1254
- genAI: {
1255
- getGenerativeModel() {
1256
- return {
1257
- async generateContentStream() {
1258
- sdkCalls += 1;
1259
- return sdkStream([{
1260
- candidates: [{ content: { role: 'model', parts: [{ text: 'uncached ok' }] }, finishReason: 'STOP' }],
1261
- }]);
1262
- },
1263
- };
1264
- },
1265
- },
1266
- }));
1267
- provider._ensureGeminiCache = async ({ skipExplicitCache }) => {
1268
- cacheChecks += 1;
1269
- return skipExplicitCache ? null : 'cachedContents/evicted';
1270
- };
1271
- const opts = {
1272
- providerState: { gemini: { cacheName: 'cachedContents/evicted', cachePrefixContentCount: 0 } },
1273
- };
1274
- const result = await provider.send(
1275
- [{ role: 'user', content: 'hello' }],
1276
- 'gemini-2.5-flash',
1277
- [],
1278
- opts,
1279
- );
1280
- assert.equal(result.content, 'uncached ok');
1281
- assert.equal(restCalls, 1);
1282
- assert.equal(sdkCalls, 1);
1283
- assert.equal(cacheChecks, 2);
1284
- assert.equal(opts.providerState.gemini, undefined);
1285
- assert.equal(geminiGlobalCaches.size, 0);
1286
- });
1287
-
1288
- test('Gemini REST rate-limit response preserves Retry-After for request retry', async () => {
1289
- let calls = 0;
1290
- let firstCallAt = 0;
1291
- let secondCallAt = 0;
1292
- const provider = new GeminiProvider(hermeticConfig({
1293
- fetchFn: async () => {
1294
- calls += 1;
1295
- if (calls === 1) {
1296
- firstCallAt = Date.now();
1297
- return {
1298
- ok: false,
1299
- status: 429,
1300
- headers: new Headers({ 'Retry-After': '0.05' }),
1301
- async text() {
1302
- return JSON.stringify({
1303
- error: {
1304
- status: 'RESOURCE_EXHAUSTED',
1305
- message: 'resource exhausted; retry after the capacity window',
1306
- details: [{
1307
- '@type': 'type.googleapis.com/google.rpc.RetryInfo',
1308
- retryDelay: '0s',
1309
- }],
1310
- },
1311
- });
1312
- },
1313
- };
1314
- }
1315
- secondCallAt = Date.now();
1316
- return {
1317
- ok: true,
1318
- body: new ReadableStream({
1319
- start(controller) {
1320
- controller.enqueue(new TextEncoder().encode(
1321
- 'data: {"candidates":[{"content":{"role":"model","parts":[{"text":"retried"}]},"finishReason":"STOP"}]}\n',
1322
- ));
1323
- controller.close();
1324
- },
1325
- }),
1326
- };
1327
- },
1328
- }));
1329
- provider._ensureGeminiCache = async () => 'cachedContents/live';
1330
- const result = await provider.send(
1331
- [{ role: 'user', content: 'hello' }],
1332
- 'gemini-2.5-flash',
1333
- [],
1334
- { providerState: { gemini: { cachePrefixContentCount: 0 } } },
1335
- );
1336
- assert.equal(result.content, 'retried');
1337
- assert.equal(calls, 2);
1338
- assert.ok(secondCallAt - firstCallAt >= 40, 'Retry-After must set the retry delay');
1339
- });
1340
-
1341
- test('Gemini structured RetryInfo outranks resource-exhausted permanence', async () => {
1342
- let calls = 0;
1343
- let firstCallAt = 0;
1344
- let secondCallAt = 0;
1345
- const provider = new GeminiProvider(hermeticConfig({
1346
- fetchFn: async () => {
1347
- calls += 1;
1348
- if (calls === 1) {
1349
- firstCallAt = Date.now();
1350
- return {
1351
- ok: false,
1352
- status: 429,
1353
- headers: new Headers(),
1354
- async text() {
1355
- return JSON.stringify({
1356
- error: {
1357
- status: 'RESOURCE_EXHAUSTED',
1358
- message: 'resource exhausted',
1359
- details: [{
1360
- '@type': 'type.googleapis.com/google.rpc.RetryInfo',
1361
- retryDelay: '0.05s',
1362
- }],
1363
- },
1364
- });
1365
- },
1366
- };
1367
- }
1368
- secondCallAt = Date.now();
1369
- return {
1370
- ok: true,
1371
- body: new ReadableStream({
1372
- start(controller) {
1373
- controller.enqueue(new TextEncoder().encode(
1374
- 'data: {"candidates":[{"content":{"role":"model","parts":[{"text":"retry-info"}]},"finishReason":"STOP"}]}\n',
1375
- ));
1376
- controller.close();
1377
- },
1378
- }),
1379
- };
1380
- },
1381
- }));
1382
- provider._ensureGeminiCache = async () => 'cachedContents/live';
1383
- const result = await provider.send(
1384
- [{ role: 'user', content: 'hello' }],
1385
- 'gemini-2.5-flash',
1386
- [],
1387
- { providerState: { gemini: { cachePrefixContentCount: 0 } } },
1388
- );
1389
- assert.equal(result.content, 'retry-info');
1390
- assert.equal(calls, 2);
1391
- assert.ok(secondCallAt - firstCallAt >= 40, 'RetryInfo.retryDelay must set the retry delay');
1392
- });
1393
-
1394
- test('Gemini RESOURCE_EXHAUSTED without Retry-After remains permanent', async () => {
1395
- let calls = 0;
1396
- const provider = new GeminiProvider(hermeticConfig({
1397
- fetchFn: async () => {
1398
- calls += 1;
1399
- return {
1400
- ok: false,
1401
- status: 429,
1402
- headers: new Headers(),
1403
- async text() {
1404
- return JSON.stringify({
1405
- error: {
1406
- status: 'RESOURCE_EXHAUSTED',
1407
- message: 'capacity quota exhausted',
1408
- details: [{ '@type': 'type.googleapis.com/google.rpc.QuotaFailure' }],
1409
- },
1410
- });
1411
- },
1412
- };
1413
- },
1414
- }));
1415
- provider._ensureGeminiCache = async () => 'cachedContents/live';
1416
- const error = await provider.send(
1417
- [{ role: 'user', content: 'hello' }],
1418
- 'gemini-2.5-flash',
1419
- [],
1420
- { providerState: { gemini: { cachePrefixContentCount: 0 } } },
1421
- ).then(() => null, (caught) => caught);
1422
- assert.equal(calls, 1);
1423
- assert.equal(error.code, 'RESOURCE_EXHAUSTED');
1424
- assert.equal(error.geminiStatus, 'RESOURCE_EXHAUSTED');
1425
- assert.equal(error.details[0]['@type'], 'type.googleapis.com/google.rpc.QuotaFailure');
1426
- assert.equal(classifyError(error), 'permanent');
1427
- });
1428
-
1429
- test('Gemini availability probe rejects an unresolved generation within its bound', async () => {
1430
- let receivedSignal = null;
1431
- const startedAt = Date.now();
1432
- const provider = new GeminiProvider(hermeticConfig({
1433
- genAI: {
1434
- getGenerativeModel() {
1435
- return {
1436
- async generateContent(_input, options) {
1437
- receivedSignal = options?.signal;
1438
- return new Promise(() => {});
1439
- },
1440
- };
1441
- },
1442
- },
1443
- }));
1444
- assert.equal(await provider.isAvailable(), false);
1445
- assert.equal(receivedSignal instanceof AbortSignal, true);
1446
- assert.equal(receivedSignal.aborted, true);
1447
- assert.ok(Date.now() - startedAt < 1_500, 'availability probe must be bounded');
1448
- });