mixdog 0.9.3 → 0.9.5
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.
- package/README.md +112 -38
- package/package.json +10 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +119 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +356 -0
- package/src/tui/app/model-options.mjs +334 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +489 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +677 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +13553 -12384
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/channels/lib/holidays.mjs +0 -138
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GoogleGenerativeAI
|
|
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 {
|
|
18
|
-
|
|
19
|
-
|
|
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 },
|
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
}
|