mixdog 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { loadConfig, getPluginData } from '../config.mjs';
|
|
6
|
+
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
7
|
+
import { withRetry } from './retry-classifier.mjs';
|
|
8
|
+
import { traceBridgeUsage, appendBridgeTrace } from '../bridge-trace.mjs';
|
|
9
|
+
import {
|
|
10
|
+
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
11
|
+
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
12
|
+
PROVIDER_MAX_BEFORE_WARN_MS,
|
|
13
|
+
providerTimeoutError,
|
|
14
|
+
resolveTimeoutMs,
|
|
15
|
+
} from '../stall-policy.mjs';
|
|
16
|
+
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
17
|
+
|
|
18
|
+
const MODELS = [
|
|
19
|
+
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
20
|
+
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
21
|
+
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
22
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'gemini', contextWindow: 1048576 },
|
|
23
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'gemini', contextWindow: 1048576 },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MODEL = MODELS[0].id;
|
|
27
|
+
|
|
28
|
+
// --- Model catalog cache (24h disk TTL) ---
|
|
29
|
+
// Gemini's /models has no `created` timestamp, so latest-resolution is
|
|
30
|
+
// VERSION-based (parse gemini-X.Y) rather than release-date based.
|
|
31
|
+
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
32
|
+
|
|
33
|
+
// De-dupes concurrent force-refreshes so they share one HTTP round-trip,
|
|
34
|
+
// mirroring anthropic-oauth's _modelRefreshInFlight.
|
|
35
|
+
let _modelRefreshInFlight = null;
|
|
36
|
+
|
|
37
|
+
function _modelCachePath() {
|
|
38
|
+
return join(getPluginData(), 'gemini-models.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _loadModelCache() {
|
|
42
|
+
const path = _modelCachePath();
|
|
43
|
+
if (!existsSync(path)) return null;
|
|
44
|
+
try {
|
|
45
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
46
|
+
if (!raw?.fetchedAt || !Array.isArray(raw.models)) return null;
|
|
47
|
+
if (Date.now() - raw.fetchedAt > MODEL_CACHE_TTL_MS) return null;
|
|
48
|
+
return raw.models;
|
|
49
|
+
} catch { return null; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _saveModelCache(models) {
|
|
53
|
+
try {
|
|
54
|
+
writeJsonAtomicSync(_modelCachePath(), { fetchedAt: Date.now(), models }, { lock: true, fsyncDir: true });
|
|
55
|
+
} catch { /* best-effort */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Mirror of anthropic-oauth.mjs _compareVersion: compare two gemini ids by the
|
|
59
|
+
// X.Y version embedded in the id (gemini-3.5-flash -> [3, 5]). Falls back to a
|
|
60
|
+
// lexicographic tiebreak so ordering is total.
|
|
61
|
+
function _compareVersion(a, b) {
|
|
62
|
+
const na = (a.match(/gemini-(\d+)(?:\.(\d+))?/) || []).slice(1).map(Number);
|
|
63
|
+
const nb = (b.match(/gemini-(\d+)(?:\.(\d+))?/) || []).slice(1).map(Number);
|
|
64
|
+
for (let i = 0; i < Math.max(na.length, nb.length); i++) {
|
|
65
|
+
if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
|
|
66
|
+
}
|
|
67
|
+
return a.localeCompare(b);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Per family, mark the highest-version model as latest:true.
|
|
71
|
+
function _markLatestGemini(models) {
|
|
72
|
+
const byFamily = new Map();
|
|
73
|
+
for (const m of models) {
|
|
74
|
+
if (!m?.id) continue;
|
|
75
|
+
const cur = byFamily.get(m.family);
|
|
76
|
+
if (!cur || _compareVersion(m.id, cur.id) > 0) {
|
|
77
|
+
byFamily.set(m.family, m);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const m of byFamily.values()) m.latest = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Newest chat model by VERSION in the 'gemini-flash' family, read from the
|
|
84
|
+
// on-disk catalog cache. Returns null until cached; callers warm via
|
|
85
|
+
// ensureLatestGeminiModel when null.
|
|
86
|
+
export function resolveLatestGeminiModel() {
|
|
87
|
+
const cached = _loadModelCache();
|
|
88
|
+
if (!Array.isArray(cached)) return null;
|
|
89
|
+
let best = null;
|
|
90
|
+
for (const m of cached) {
|
|
91
|
+
if (!m?.id || m.family !== 'gemini-flash') continue;
|
|
92
|
+
if (!best || _compareVersion(m.id, best.id) > 0) best = m;
|
|
93
|
+
}
|
|
94
|
+
return best?.id || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function ensureLatestGeminiModel(provider) {
|
|
98
|
+
let m = resolveLatestGeminiModel();
|
|
99
|
+
if (m) return m;
|
|
100
|
+
await provider._refreshModelCache();
|
|
101
|
+
m = resolveLatestGeminiModel();
|
|
102
|
+
if (m) return m;
|
|
103
|
+
throw new Error('[gemini] model catalog unavailable after warmup — cannot resolve default model');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const GEMINI_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
|
|
107
|
+
'MIXDOG_GEMINI_FIRST_BYTE_TIMEOUT_MS',
|
|
108
|
+
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
109
|
+
{ minMs: 30_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
110
|
+
);
|
|
111
|
+
const GEMINI_GENERATE_TOTAL_TIMEOUT_MS = resolveTimeoutMs(
|
|
112
|
+
'MIXDOG_GEMINI_GENERATE_TOTAL_TIMEOUT_MS',
|
|
113
|
+
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
114
|
+
{ minMs: 30_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
function traceHash(value) {
|
|
118
|
+
return createHash('sha256')
|
|
119
|
+
.update(String(value ?? ''))
|
|
120
|
+
.digest('hex')
|
|
121
|
+
.slice(0, 16);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function stableTraceStringify(value, seen = new WeakSet()) {
|
|
125
|
+
if (value === null || typeof value !== 'object') {
|
|
126
|
+
if (typeof value === 'bigint') return JSON.stringify(String(value));
|
|
127
|
+
if (typeof value === 'undefined' || typeof value === 'function') return 'null';
|
|
128
|
+
return JSON.stringify(value);
|
|
129
|
+
}
|
|
130
|
+
if (seen.has(value)) return JSON.stringify('[Circular]');
|
|
131
|
+
seen.add(value);
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
const serialized = '[' + value.map(v => stableTraceStringify(v, seen)).join(',') + ']';
|
|
134
|
+
seen.delete(value);
|
|
135
|
+
return serialized;
|
|
136
|
+
}
|
|
137
|
+
const parts = [];
|
|
138
|
+
for (const key of Object.keys(value).sort()) {
|
|
139
|
+
const v = value[key];
|
|
140
|
+
if (typeof v === 'undefined' || typeof v === 'function') continue;
|
|
141
|
+
parts.push(JSON.stringify(key) + ':' + stableTraceStringify(v, seen));
|
|
142
|
+
}
|
|
143
|
+
seen.delete(value);
|
|
144
|
+
return '{' + parts.join(',') + '}';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function traceTextShape(text) {
|
|
148
|
+
const value = String(text ?? '');
|
|
149
|
+
return { chars: value.length, hash: traceHash(value) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function summarizeTracePart(part) {
|
|
153
|
+
if (!part || typeof part !== 'object') return { type: typeof part };
|
|
154
|
+
if ('text' in part) {
|
|
155
|
+
return { type: 'text', ...traceTextShape(part.text) };
|
|
156
|
+
}
|
|
157
|
+
if (part.functionCall) {
|
|
158
|
+
return {
|
|
159
|
+
type: 'functionCall',
|
|
160
|
+
name: part.functionCall.name || null,
|
|
161
|
+
argsHash: traceHash(stableTraceStringify(part.functionCall.args || {})),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (part.functionResponse) {
|
|
165
|
+
const response = part.functionResponse.response || {};
|
|
166
|
+
const responseShape = stableTraceStringify(response);
|
|
167
|
+
return {
|
|
168
|
+
type: 'functionResponse',
|
|
169
|
+
name: part.functionResponse.name || null,
|
|
170
|
+
responseChars: responseShape.length,
|
|
171
|
+
responseHash: traceHash(responseShape),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { type: Object.keys(part).sort().join('|') || 'unknown' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function summarizeTraceContents(contents) {
|
|
178
|
+
const summaries = (contents || []).map((content, index) => ({
|
|
179
|
+
index,
|
|
180
|
+
role: content?.role || null,
|
|
181
|
+
parts: Array.isArray(content?.parts) ? content.parts.map(summarizeTracePart) : [],
|
|
182
|
+
}));
|
|
183
|
+
if (summaries.length <= 12) return summaries;
|
|
184
|
+
return [
|
|
185
|
+
...summaries.slice(0, 8),
|
|
186
|
+
{ omittedTurns: summaries.length - 12 },
|
|
187
|
+
...summaries.slice(-4),
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function summarizeTraceTools(tools) {
|
|
192
|
+
return (tools || []).map(t => ({
|
|
193
|
+
name: t?.name || null,
|
|
194
|
+
description: t?.description || '',
|
|
195
|
+
inputSchema: t?.inputSchema || null,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Gemini cachedContents API rejects prefixes below the model-specific minimum
|
|
200
|
+
// (Gemini 2.0/2.5 = 2048 tokens, Gemini 3.x = 4096 tokens) with HTTP 400
|
|
201
|
+
// "Cached content is too small". Estimating chars/4 ≈ tokens lets us skip the
|
|
202
|
+
// roundtrip when the prefix cannot satisfy the invariant. Estimate undercount
|
|
203
|
+
// is harmless (we attempt and get the same 400 we would have skipped); overcount
|
|
204
|
+
// is harmless (we skip, fall back to implicit cache).
|
|
205
|
+
function _estimateGeminiCacheTokens(systemInstruction, geminiTools, contents) {
|
|
206
|
+
let chars = 0;
|
|
207
|
+
if (typeof systemInstruction === 'string') chars += systemInstruction.length;
|
|
208
|
+
if (Array.isArray(geminiTools) && geminiTools.length) {
|
|
209
|
+
try { chars += JSON.stringify(geminiTools).length; } catch {}
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(contents) && contents.length > 1) {
|
|
212
|
+
for (let i = 0; i < contents.length - 1; i++) {
|
|
213
|
+
try { chars += JSON.stringify(contents[i]?.parts ?? '').length; } catch {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return Math.ceil(chars / 4);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function _geminiCacheMinTokens(model) {
|
|
220
|
+
return /^gemini-3/i.test(String(model || '')) ? 4096 : 2048;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function _geminiCachePrefixCount(contents) {
|
|
224
|
+
return Array.isArray(contents) && contents.length > 1 ? contents.length - 1 : 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function _geminiCachePrefixContents(contents, prefixCount) {
|
|
228
|
+
if (!Array.isArray(contents) || prefixCount <= 0) return [];
|
|
229
|
+
return contents.slice(0, prefixCount).map(c => {
|
|
230
|
+
const r = c?.role;
|
|
231
|
+
const safeRole = (r === 'model' || r === 'user') ? r : 'user';
|
|
232
|
+
return {
|
|
233
|
+
role: safeRole,
|
|
234
|
+
parts: Array.isArray(c?.parts) ? c.parts : [],
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function _geminiCachePrefixHash({ model, systemInstruction, geminiTools, contents, prefixCount }) {
|
|
240
|
+
return traceHash(stableTraceStringify({
|
|
241
|
+
model: model || null,
|
|
242
|
+
systemInstruction: systemInstruction || '',
|
|
243
|
+
tools: geminiTools || [],
|
|
244
|
+
contents: _geminiCachePrefixContents(contents, prefixCount),
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _resolveGeminiCacheUsage({ usageMetadata, cachedContent, providerState }) {
|
|
249
|
+
const inputTokens = Number(usageMetadata?.promptTokenCount || usageMetadata?.totalTokenCount || 0) || 0;
|
|
250
|
+
const reportedCachedTokens = Number(usageMetadata?.cachedContentTokenCount || 0) || 0;
|
|
251
|
+
const cachedFallbackTokens = cachedContent
|
|
252
|
+
? Number(providerState?.gemini?.cacheTokenSize || 0) || 0
|
|
253
|
+
: 0;
|
|
254
|
+
const rawCachedTokens = reportedCachedTokens > 0 ? reportedCachedTokens : cachedFallbackTokens;
|
|
255
|
+
const cachedTokens = inputTokens > 0 ? Math.min(rawCachedTokens, inputTokens) : rawCachedTokens;
|
|
256
|
+
const cacheTokenSource = reportedCachedTokens > 0
|
|
257
|
+
? 'usage_metadata'
|
|
258
|
+
: (cachedFallbackTokens > 0 ? 'cache_create_fallback' : 'none');
|
|
259
|
+
return {
|
|
260
|
+
inputTokens,
|
|
261
|
+
reportedCachedTokens,
|
|
262
|
+
cachedFallbackTokens,
|
|
263
|
+
cachedTokens,
|
|
264
|
+
cacheTokenSource,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function writeGeminiCacheTrace({ opts, model, systemInstruction, tools, contents, usageMetadata, cachedContent }) {
|
|
269
|
+
if (process.env.MIXDOG_GEMINI_CACHE_TRACE !== '1') return;
|
|
270
|
+
try {
|
|
271
|
+
const session = opts?.session || {};
|
|
272
|
+
const {
|
|
273
|
+
inputTokens,
|
|
274
|
+
reportedCachedTokens,
|
|
275
|
+
cachedFallbackTokens,
|
|
276
|
+
cachedTokens,
|
|
277
|
+
cacheTokenSource,
|
|
278
|
+
} = _resolveGeminiCacheUsage({
|
|
279
|
+
usageMetadata,
|
|
280
|
+
cachedContent,
|
|
281
|
+
providerState: opts?.providerState,
|
|
282
|
+
});
|
|
283
|
+
const toolShape = summarizeTraceTools(tools);
|
|
284
|
+
const trace = {
|
|
285
|
+
event: 'generate',
|
|
286
|
+
provider: 'gemini',
|
|
287
|
+
model,
|
|
288
|
+
owner: session.owner || null,
|
|
289
|
+
role: session.role || null,
|
|
290
|
+
permission: session.permission || null,
|
|
291
|
+
toolPermission: session.toolPermission || null,
|
|
292
|
+
profileId: session.profileId || null,
|
|
293
|
+
sourceType: session.sourceType || null,
|
|
294
|
+
sourceName: session.sourceName || null,
|
|
295
|
+
sessionIdHash: opts?.sessionId ? traceHash(opts.sessionId) : null,
|
|
296
|
+
providerCacheKeyHash: opts?.providerCacheKey ? traceHash(opts.providerCacheKey) : null,
|
|
297
|
+
promptCacheKeyHash: opts?.promptCacheKey ? traceHash(opts.promptCacheKey) : null,
|
|
298
|
+
systemChars: systemInstruction ? systemInstruction.length : 0,
|
|
299
|
+
systemHash: systemInstruction ? traceHash(systemInstruction) : null,
|
|
300
|
+
toolCount: Array.isArray(tools) ? tools.length : 0,
|
|
301
|
+
toolSchemaHash: traceHash(stableTraceStringify(toolShape)),
|
|
302
|
+
contentTurnCount: Array.isArray(contents) ? contents.length : 0,
|
|
303
|
+
contents: summarizeTraceContents(contents),
|
|
304
|
+
inputTokens,
|
|
305
|
+
cachedTokens,
|
|
306
|
+
reportedCachedTokens,
|
|
307
|
+
cachedFallbackTokens,
|
|
308
|
+
cacheTokenSource,
|
|
309
|
+
cacheAttached: !!cachedContent,
|
|
310
|
+
cachePrefixContentCount: opts?.providerState?.gemini?.cachePrefixContentCount ?? null,
|
|
311
|
+
cacheHitRate: inputTokens > 0 ? Number((cachedTokens / inputTokens).toFixed(6)) : null,
|
|
312
|
+
};
|
|
313
|
+
process.stderr.write(`[gemini-cache-trace] ${JSON.stringify(trace)}\n`);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
process.stderr.write(`[gemini-cache-trace] failed: ${err?.message || err}\n`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function geminiTimeoutError(label, timeoutMs) {
|
|
320
|
+
const err = providerTimeoutError(label, timeoutMs);
|
|
321
|
+
err.name = 'GeminiTimeoutError';
|
|
322
|
+
err.code = 'EGEMINITIMEOUT';
|
|
323
|
+
return err;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function runGeminiOperationWithTimeout({ label, timeoutMs, signal, run }) {
|
|
327
|
+
const ac = new AbortController();
|
|
328
|
+
let settled = false;
|
|
329
|
+
let timer = null;
|
|
330
|
+
let abortListener = null;
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
const finish = (fn, value) => {
|
|
333
|
+
if (settled) return;
|
|
334
|
+
settled = true;
|
|
335
|
+
if (timer) clearTimeout(timer);
|
|
336
|
+
if (abortListener && signal) {
|
|
337
|
+
try { signal.removeEventListener('abort', abortListener); } catch {}
|
|
338
|
+
}
|
|
339
|
+
fn(value);
|
|
340
|
+
};
|
|
341
|
+
const abort = (reason) => {
|
|
342
|
+
try { ac.abort(reason); } catch {}
|
|
343
|
+
finish(reject, reason instanceof Error ? reason : new Error(String(reason || `${label} aborted`)));
|
|
344
|
+
};
|
|
345
|
+
if (signal) {
|
|
346
|
+
abortListener = () => abort(signal.reason);
|
|
347
|
+
if (signal.aborted) { abortListener(); return; }
|
|
348
|
+
signal.addEventListener('abort', abortListener, { once: true });
|
|
349
|
+
}
|
|
350
|
+
timer = setTimeout(() => {
|
|
351
|
+
abort(geminiTimeoutError(label, timeoutMs));
|
|
352
|
+
}, timeoutMs);
|
|
353
|
+
if (timer.unref) timer.unref();
|
|
354
|
+
Promise.resolve()
|
|
355
|
+
.then(() => run(ac.signal))
|
|
356
|
+
.then((value) => finish(resolve, value), (err) => finish(reject, err));
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Convert JSON Schema type string to Gemini SchemaType.
|
|
362
|
+
* Gemini SDK uses its own enum instead of plain strings.
|
|
363
|
+
*/
|
|
364
|
+
function toSchemaType(t) {
|
|
365
|
+
const map = {
|
|
366
|
+
string: SchemaType.STRING,
|
|
367
|
+
number: SchemaType.NUMBER,
|
|
368
|
+
integer: SchemaType.INTEGER,
|
|
369
|
+
boolean: SchemaType.BOOLEAN,
|
|
370
|
+
array: SchemaType.ARRAY,
|
|
371
|
+
object: SchemaType.OBJECT,
|
|
372
|
+
};
|
|
373
|
+
return map[t] ?? SchemaType.STRING;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Recursively convert a JSON Schema object to Gemini's FunctionDeclarationSchema.
|
|
378
|
+
* Gemini requires `type` to be a SchemaType enum, not a plain string, and
|
|
379
|
+
* rejects several JSON Schema fields the API does not understand
|
|
380
|
+
* (additionalProperties, $schema, $ref, const, examples, definitions,
|
|
381
|
+
* patternProperties). We strip those at every level.
|
|
382
|
+
*/
|
|
383
|
+
const GEMINI_SCHEMA_STRIP = new Set([
|
|
384
|
+
'additionalProperties',
|
|
385
|
+
'$schema',
|
|
386
|
+
'$ref',
|
|
387
|
+
'const',
|
|
388
|
+
'examples',
|
|
389
|
+
'definitions',
|
|
390
|
+
'patternProperties',
|
|
391
|
+
]);
|
|
392
|
+
function convertSchema(schema) {
|
|
393
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
394
|
+
const result = {};
|
|
395
|
+
for (const [k, v] of Object.entries(schema)) {
|
|
396
|
+
if (GEMINI_SCHEMA_STRIP.has(k)) continue;
|
|
397
|
+
result[k] = v;
|
|
398
|
+
}
|
|
399
|
+
// Gemini's Schema validator requires every `enum` entry to be a string,
|
|
400
|
+
// even when the parent `type` is integer/number/boolean. Drop the enum in
|
|
401
|
+
// that case rather than emit an invalid typed enum — `type` plus the
|
|
402
|
+
// description still guides the model, and the tool handler revalidates.
|
|
403
|
+
const rawType = typeof result.type === 'string' ? result.type : undefined;
|
|
404
|
+
if (Array.isArray(result.enum) && (rawType === 'integer' || rawType === 'number' || rawType === 'boolean')) {
|
|
405
|
+
if (result.enum.some((item) => typeof item !== 'string')) {
|
|
406
|
+
delete result.enum;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Gemini rejects array schemas that omit `items`; fill a permissive
|
|
410
|
+
// default so the declaration validates.
|
|
411
|
+
if (rawType === 'array' && (!result.items || typeof result.items !== 'object')) {
|
|
412
|
+
result.items = { type: 'string' };
|
|
413
|
+
}
|
|
414
|
+
if (typeof result.type === 'string') {
|
|
415
|
+
result.type = toSchemaType(result.type);
|
|
416
|
+
}
|
|
417
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
418
|
+
const props = {};
|
|
419
|
+
for (const [key, val] of Object.entries(result.properties)) {
|
|
420
|
+
props[key] = convertSchema(val);
|
|
421
|
+
}
|
|
422
|
+
result.properties = props;
|
|
423
|
+
}
|
|
424
|
+
if (result.items && typeof result.items === 'object') {
|
|
425
|
+
result.items = convertSchema(result.items);
|
|
426
|
+
}
|
|
427
|
+
// Recurse into JSON Schema combinator keys so disallowed fields
|
|
428
|
+
// (additionalProperties, $schema, etc.) get stripped at every nesting
|
|
429
|
+
// level. Without this, schemas using anyOf/oneOf/allOf/not pass the
|
|
430
|
+
// shallow strip but fail Gemini validation at depth.
|
|
431
|
+
//
|
|
432
|
+
// Two Gemini-specific normalizations are also applied per combinator
|
|
433
|
+
// subschema:
|
|
434
|
+
// 1. Inject `type: OBJECT` when a subschema uses object-only keys
|
|
435
|
+
// (`required` / `properties`) without an explicit type — Gemini
|
|
436
|
+
// rejects `required` outside of OBJECT type.
|
|
437
|
+
// 2. Materialize a local `properties` map from the parent's properties
|
|
438
|
+
// when the subschema only carries `required: [names]` — Gemini
|
|
439
|
+
// validates that every name in `required` exists in *this*
|
|
440
|
+
// subschema's `properties` (it does not inherit from the parent
|
|
441
|
+
// the way JSON Schema's compositional model does).
|
|
442
|
+
for (const combinator of ['anyOf', 'oneOf', 'allOf']) {
|
|
443
|
+
if (Array.isArray(result[combinator])) {
|
|
444
|
+
result[combinator] = result[combinator].map((s) => {
|
|
445
|
+
const sub = convertSchema(s);
|
|
446
|
+
if (sub && typeof sub === 'object') {
|
|
447
|
+
const usesObjectKeys = sub.required !== undefined || sub.properties !== undefined;
|
|
448
|
+
if (usesObjectKeys && sub.type === undefined) {
|
|
449
|
+
sub.type = toSchemaType('object');
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(sub.required) && !sub.properties && result.properties) {
|
|
452
|
+
const projected = {};
|
|
453
|
+
for (const k of sub.required) {
|
|
454
|
+
if (result.properties[k]) projected[k] = result.properties[k];
|
|
455
|
+
}
|
|
456
|
+
if (Object.keys(projected).length > 0) sub.properties = projected;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return sub;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (result.not && typeof result.not === 'object') {
|
|
464
|
+
result.not = convertSchema(result.not);
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function toGeminiTools(tools) {
|
|
470
|
+
return {
|
|
471
|
+
functionDeclarations: tools.map((t) => ({
|
|
472
|
+
name: t.name,
|
|
473
|
+
description: t.description,
|
|
474
|
+
parameters: convertSchema(t.inputSchema),
|
|
475
|
+
})),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Map the orchestrator-level toolChoice to Gemini's functionCallingConfig.
|
|
480
|
+
// auto -> AUTO
|
|
481
|
+
// required -> ANY
|
|
482
|
+
// none -> NONE
|
|
483
|
+
// { name } -> ANY + allowedFunctionNames:[name] (specific tool)
|
|
484
|
+
function toGeminiToolConfig(toolChoice) {
|
|
485
|
+
if (toolChoice == null) return undefined;
|
|
486
|
+
if (typeof toolChoice === 'string') {
|
|
487
|
+
if (toolChoice === 'auto') return { functionCallingConfig: { mode: 'AUTO' } };
|
|
488
|
+
if (toolChoice === 'required') return { functionCallingConfig: { mode: 'ANY' } };
|
|
489
|
+
if (toolChoice === 'none') return { functionCallingConfig: { mode: 'NONE' } };
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
if (typeof toolChoice === 'object') {
|
|
493
|
+
const name = toolChoice.name || toolChoice.function?.name;
|
|
494
|
+
if (typeof name === 'string' && name) {
|
|
495
|
+
return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [name] } };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function toGeminiContent(message, toolNameByCallId) {
|
|
502
|
+
if (!message || message.role === 'system') return null;
|
|
503
|
+
if (message.role === 'assistant' && message.toolCalls?.length) {
|
|
504
|
+
const parts = [];
|
|
505
|
+
if (message.content) parts.push({ text: message.content });
|
|
506
|
+
for (const tc of message.toolCalls) {
|
|
507
|
+
// Gemini 3 thinking models require the original thoughtSignature
|
|
508
|
+
// echoed back on every prior functionCall so the cached thinking
|
|
509
|
+
// prefix stays valid. v1beta places the field at the Part level
|
|
510
|
+
// (sibling of functionCall) — putting it inside functionCall returns
|
|
511
|
+
// 400 "Unknown name". Older models / first turn have no signature.
|
|
512
|
+
const part = { functionCall: { name: tc.name, args: tc.arguments } };
|
|
513
|
+
if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;
|
|
514
|
+
parts.push(part);
|
|
515
|
+
}
|
|
516
|
+
return { role: 'model', parts };
|
|
517
|
+
}
|
|
518
|
+
if (message.role === 'tool') {
|
|
519
|
+
// Tool result content stays byte-identical for cache prefix stability.
|
|
520
|
+
// Gemini accepts functionResponse parts under role 'user' (per docs).
|
|
521
|
+
// Using 'user' keeps tool_result entries byte-identical between
|
|
522
|
+
// cachedContents.create (which rejects role:'function') and
|
|
523
|
+
// generateContent, so the cached prefix actually matches at runtime.
|
|
524
|
+
// functionResponse.name must be the FUNCTION name, not the synthetic
|
|
525
|
+
// toolCallId. Resolve it from the toolCallId->functionName map built
|
|
526
|
+
// from prior assistant tool_calls; fall back to the raw id only when
|
|
527
|
+
// no mapping exists.
|
|
528
|
+
const functionName = (toolNameByCallId && toolNameByCallId.get(message.toolCallId))
|
|
529
|
+
|| message.toolCallId
|
|
530
|
+
|| '';
|
|
531
|
+
return {
|
|
532
|
+
role: 'user',
|
|
533
|
+
parts: [{ functionResponse: { name: functionName, response: { result: message.content } } }],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
role: message.role === 'assistant' ? 'model' : 'user',
|
|
538
|
+
parts: [{ text: message.content }],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function toGeminiContents(messages) {
|
|
543
|
+
const contents = [];
|
|
544
|
+
// Map synthetic toolCallId -> function name from prior assistant
|
|
545
|
+
// tool_calls so each functionResponse part carries the real function name.
|
|
546
|
+
const toolNameByCallId = new Map();
|
|
547
|
+
for (const m of messages) {
|
|
548
|
+
if (m?.role === 'assistant' && Array.isArray(m.toolCalls)) {
|
|
549
|
+
for (const tc of m.toolCalls) {
|
|
550
|
+
if (tc?.id && tc?.name) toolNameByCallId.set(tc.id, tc.name);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
for (const message of messages) {
|
|
555
|
+
const content = toGeminiContent(message, toolNameByCallId);
|
|
556
|
+
if (content) contents.push(content);
|
|
557
|
+
}
|
|
558
|
+
return contents;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function parseToolCalls(parts) {
|
|
562
|
+
const calls = parts.filter((p) => 'functionCall' in p && !!p.functionCall);
|
|
563
|
+
if (!calls.length)
|
|
564
|
+
return undefined;
|
|
565
|
+
// The @google/generative-ai 0.24.1 SDK predates Gemini 3 thinking — its
|
|
566
|
+
// FunctionCall type only declares { name, args }. The runtime object,
|
|
567
|
+
// however, retains whatever the wire response carried, which means the
|
|
568
|
+
// signature may sit under any of:
|
|
569
|
+
// • part.functionCall.thoughtSignature (camelCase, expected)
|
|
570
|
+
// • part.functionCall.thought_signature (snake_case, raw protobuf)
|
|
571
|
+
// • part.thoughtSignature / part.thought_signature (sibling on Part)
|
|
572
|
+
// Read all four and use the first non-empty hit. Set MIXDOG_DEBUG_GEMINI=1
|
|
573
|
+
// to dump the raw parts so we can confirm the actual key location on the
|
|
574
|
+
// next session and harden the parser.
|
|
575
|
+
if (process.env.MIXDOG_DEBUG_GEMINI === '1') {
|
|
576
|
+
try { process.stderr.write(`[gemini fc raw] ${JSON.stringify(parts)}\n`); } catch {}
|
|
577
|
+
}
|
|
578
|
+
return calls.map((p, i) => {
|
|
579
|
+
const fc = p.functionCall;
|
|
580
|
+
const sig = fc.thoughtSignature
|
|
581
|
+
|| fc.thought_signature
|
|
582
|
+
|| p.thoughtSignature
|
|
583
|
+
|| p.thought_signature
|
|
584
|
+
|| null;
|
|
585
|
+
const call = {
|
|
586
|
+
id: `gemini_${Date.now()}_${i}`,
|
|
587
|
+
name: fc.name,
|
|
588
|
+
arguments: (fc.args ?? {}),
|
|
589
|
+
};
|
|
590
|
+
if (sig) call.thoughtSignature = sig;
|
|
591
|
+
return call;
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export class GeminiProvider {
|
|
596
|
+
// promptTokenCount is the total (cachedContentTokenCount is a subset), so
|
|
597
|
+
// input already includes cache. See registry.mjs.
|
|
598
|
+
static inputExcludesCache = false;
|
|
599
|
+
name = 'gemini';
|
|
600
|
+
genAI;
|
|
601
|
+
config;
|
|
602
|
+
|
|
603
|
+
constructor(config) {
|
|
604
|
+
this.config = config;
|
|
605
|
+
const apiKey = config.apiKey || process.env.GEMINI_API_KEY || '';
|
|
606
|
+
this.genAI = new GoogleGenerativeAI(apiKey);
|
|
607
|
+
// Warm a kept-alive socket to the Gemini REST API so the first cache/
|
|
608
|
+
// generateContent request skips the cold TLS handshake. Best-effort.
|
|
609
|
+
preconnect('https://generativelanguage.googleapis.com');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
reloadApiKey() {
|
|
613
|
+
try {
|
|
614
|
+
const freshConfig = loadConfig();
|
|
615
|
+
const cfg = freshConfig.providers?.gemini;
|
|
616
|
+
const newKey = cfg?.apiKey || process.env.GEMINI_API_KEY;
|
|
617
|
+
if (newKey) {
|
|
618
|
+
this.genAI = new GoogleGenerativeAI(newKey);
|
|
619
|
+
}
|
|
620
|
+
} catch { /* best effort */ }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
_getApiKey() {
|
|
624
|
+
return this.config?.apiKey || process.env.GEMINI_API_KEY || '';
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Explicit cachedContents API. The implicit cache layer on Gemini 3.x
|
|
628
|
+
// does not surface cachedContentTokenCount in usageMetadata, so the only
|
|
629
|
+
// way to obtain measurable + billable cache savings is to register the
|
|
630
|
+
// stable prefix (system + tools) as a CachedContent and pass its name on
|
|
631
|
+
// every generateContent call. TTL is 1h so a single worker session keeps
|
|
632
|
+
// one cache slot warm without re-creation overhead; storage cost (~$0.5/M
|
|
633
|
+
// tokens/hour) is dwarfed by the 75% input-price discount on hits beyond
|
|
634
|
+
// a few iterations.
|
|
635
|
+
async _ensureGeminiCache({ apiKey, model, systemInstruction, geminiTools, contents, opts }) {
|
|
636
|
+
const state = opts.providerState?.gemini || null;
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
const currentIter = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : 1;
|
|
639
|
+
const refreshEveryN = Number(process.env.MIXDOG_GEMINI_CACHE_REFRESH_EVERY) > 0
|
|
640
|
+
? Number(process.env.MIXDOG_GEMINI_CACHE_REFRESH_EVERY)
|
|
641
|
+
: 4;
|
|
642
|
+
const cacheLiveMs = state?.cacheExpiresAt ? state.cacheExpiresAt - now : 0;
|
|
643
|
+
const itersSinceCreate = state?.cacheCreatedAtIter != null
|
|
644
|
+
? currentIter - state.cacheCreatedAtIter
|
|
645
|
+
: Infinity;
|
|
646
|
+
const statePrefixContentCount = Number.isFinite(Number(state?.cachePrefixContentCount))
|
|
647
|
+
? Math.max(0, Math.trunc(Number(state.cachePrefixContentCount)))
|
|
648
|
+
: null;
|
|
649
|
+
const currentStatePrefixHash = statePrefixContentCount != null
|
|
650
|
+
? _geminiCachePrefixHash({
|
|
651
|
+
model,
|
|
652
|
+
systemInstruction,
|
|
653
|
+
geminiTools,
|
|
654
|
+
contents,
|
|
655
|
+
prefixCount: statePrefixContentCount,
|
|
656
|
+
})
|
|
657
|
+
: null;
|
|
658
|
+
const modelMatches = !!state?.cacheName && state?.cacheModel === model;
|
|
659
|
+
const prefixMatches = !!state?.cacheName
|
|
660
|
+
&& statePrefixContentCount != null
|
|
661
|
+
&& statePrefixContentCount <= (Array.isArray(contents) ? contents.length : 0)
|
|
662
|
+
&& !!state?.cachePrefixHash
|
|
663
|
+
&& state.cachePrefixHash === currentStatePrefixHash;
|
|
664
|
+
const canAttachState = !!state?.cacheName && cacheLiveMs > 0 && modelMatches && prefixMatches;
|
|
665
|
+
const canReuseState = canAttachState && cacheLiveMs > 6 * 60 * 1000 && itersSinceCreate < refreshEveryN;
|
|
666
|
+
try {
|
|
667
|
+
appendBridgeTrace({
|
|
668
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
669
|
+
iteration: currentIter,
|
|
670
|
+
kind: 'gemini_cache_decision',
|
|
671
|
+
payload: {
|
|
672
|
+
hasState: !!state?.cacheName,
|
|
673
|
+
stateCacheName: state?.cacheName || null,
|
|
674
|
+
stateCreatedAtIter: state?.cacheCreatedAtIter ?? null,
|
|
675
|
+
stateCacheModel: state?.cacheModel || null,
|
|
676
|
+
statePrefixContentCount,
|
|
677
|
+
statePrefixHash: state?.cachePrefixHash || null,
|
|
678
|
+
currentStatePrefixHash,
|
|
679
|
+
modelMatches,
|
|
680
|
+
prefixMatches,
|
|
681
|
+
canAttachState,
|
|
682
|
+
cacheLiveMs,
|
|
683
|
+
itersSinceCreate,
|
|
684
|
+
refreshEveryN,
|
|
685
|
+
decision: canReuseState ? 'reuse' : 'rebuild',
|
|
686
|
+
contentsLen: Array.isArray(contents) ? contents.length : 0,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
} catch {}
|
|
690
|
+
if (canReuseState) {
|
|
691
|
+
return state.cacheName;
|
|
692
|
+
}
|
|
693
|
+
if (!apiKey) return null;
|
|
694
|
+
// Pre-flight invariant: cachedContents.create rejects prefixes below
|
|
695
|
+
// the model-specific minimum. Skip the POST entirely when the estimate
|
|
696
|
+
// is under threshold so we don't spam 400 responses turn-after-turn.
|
|
697
|
+
const minTokens = _geminiCacheMinTokens(model);
|
|
698
|
+
const estimatedTokens = _estimateGeminiCacheTokens(systemInstruction, geminiTools, contents);
|
|
699
|
+
if (estimatedTokens < minTokens) {
|
|
700
|
+
try {
|
|
701
|
+
appendBridgeTrace({
|
|
702
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
703
|
+
iteration: currentIter,
|
|
704
|
+
kind: 'gemini_cache_skip',
|
|
705
|
+
payload: {
|
|
706
|
+
reason: 'prefix_below_min',
|
|
707
|
+
estimatedTokens,
|
|
708
|
+
minTokens,
|
|
709
|
+
model,
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
} catch {}
|
|
713
|
+
return canAttachState ? state.cacheName : null;
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
const ttlSeconds = 3600;
|
|
717
|
+
const cachePrefixContentCount = _geminiCachePrefixCount(contents);
|
|
718
|
+
const cachePrefixHash = _geminiCachePrefixHash({
|
|
719
|
+
model,
|
|
720
|
+
systemInstruction,
|
|
721
|
+
geminiTools,
|
|
722
|
+
contents,
|
|
723
|
+
prefixCount: cachePrefixContentCount,
|
|
724
|
+
});
|
|
725
|
+
const cachePrefixContents = _geminiCachePrefixContents(contents, cachePrefixContentCount);
|
|
726
|
+
const body = {
|
|
727
|
+
model: `models/${model}`,
|
|
728
|
+
ttl: `${ttlSeconds}s`,
|
|
729
|
+
};
|
|
730
|
+
if (systemInstruction) {
|
|
731
|
+
body.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
732
|
+
}
|
|
733
|
+
if (Array.isArray(geminiTools) && geminiTools.length) {
|
|
734
|
+
body.tools = geminiTools;
|
|
735
|
+
}
|
|
736
|
+
// Capture conversation prefix (everything except the latest user/
|
|
737
|
+
// tool input that the generateContent call will carry) inside the
|
|
738
|
+
// cache. cachedContents only accepts role='user' or 'model';
|
|
739
|
+
// generateContent uses role='function' for tool_result turns, so
|
|
740
|
+
// collapse that to 'user' (functionResponse parts remain inside).
|
|
741
|
+
if (cachePrefixContents.length) {
|
|
742
|
+
body.contents = cachePrefixContents;
|
|
743
|
+
}
|
|
744
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/cachedContents?key=${encodeURIComponent(apiKey)}`;
|
|
745
|
+
// Honor the external session abort signal during cache creation, not
|
|
746
|
+
// only the 20s ceiling. Without merging opts.signal a session that is
|
|
747
|
+
// aborted (stall-watchdog / closeSession) mid-cache-create leaves this
|
|
748
|
+
// preflight request running until its own timeout fires.
|
|
749
|
+
const res = await fetch(url, {
|
|
750
|
+
method: 'POST',
|
|
751
|
+
headers: { 'Content-Type': 'application/json' },
|
|
752
|
+
body: JSON.stringify(body),
|
|
753
|
+
signal: opts.signal
|
|
754
|
+
? AbortSignal.any([opts.signal, AbortSignal.timeout(20_000)])
|
|
755
|
+
: AbortSignal.timeout(20_000),
|
|
756
|
+
dispatcher: getLlmDispatcher(),
|
|
757
|
+
});
|
|
758
|
+
if (!res.ok) {
|
|
759
|
+
const text = await res.text().catch(() => '');
|
|
760
|
+
try {
|
|
761
|
+
appendBridgeTrace({
|
|
762
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
763
|
+
iteration: currentIter,
|
|
764
|
+
kind: 'gemini_cache_create_fail',
|
|
765
|
+
payload: {
|
|
766
|
+
status: res.status,
|
|
767
|
+
body: text.slice(0, 500),
|
|
768
|
+
contentsLen: Array.isArray(contents) ? contents.length : 0,
|
|
769
|
+
cachePrefixContentCount,
|
|
770
|
+
canAttachState,
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
} catch {}
|
|
774
|
+
return canAttachState ? state.cacheName : null;
|
|
775
|
+
}
|
|
776
|
+
const data = await res.json();
|
|
777
|
+
const cacheName = data?.name || null;
|
|
778
|
+
if (!cacheName) return canAttachState ? state.cacheName : null;
|
|
779
|
+
const cacheTokenSize = Number(data?.usageMetadata?.totalTokenCount || 0) || 0;
|
|
780
|
+
try {
|
|
781
|
+
appendBridgeTrace({
|
|
782
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
783
|
+
iteration: currentIter,
|
|
784
|
+
kind: 'gemini_cache_create_ok',
|
|
785
|
+
payload: {
|
|
786
|
+
cacheName,
|
|
787
|
+
cacheTokenSize,
|
|
788
|
+
contentsLen: Array.isArray(contents) ? contents.length : 0,
|
|
789
|
+
cachePrefixContentCount,
|
|
790
|
+
cachePrefixHash,
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
} catch {}
|
|
794
|
+
// Best-effort cleanup of the previous cache so storage cost only
|
|
795
|
+
// accrues on the live revision. Fire-and-forget; TTL expiry covers
|
|
796
|
+
// any delete failures.
|
|
797
|
+
const priorCacheName = state?.cacheName || null;
|
|
798
|
+
if (priorCacheName && priorCacheName !== cacheName) {
|
|
799
|
+
const delUrl = `https://generativelanguage.googleapis.com/v1beta/${priorCacheName}?key=${encodeURIComponent(apiKey)}`;
|
|
800
|
+
fetch(delUrl, { method: 'DELETE', signal: AbortSignal.timeout(10_000), dispatcher: getLlmDispatcher() })
|
|
801
|
+
.catch(() => { /* TTL expiry will reclaim it */ });
|
|
802
|
+
}
|
|
803
|
+
opts.providerState = {
|
|
804
|
+
...(opts.providerState || {}),
|
|
805
|
+
gemini: {
|
|
806
|
+
cacheName,
|
|
807
|
+
cacheCreatedAt: now,
|
|
808
|
+
cacheCreatedAtIter: currentIter,
|
|
809
|
+
cacheExpiresAt: now + ttlSeconds * 1000,
|
|
810
|
+
cacheModel: model,
|
|
811
|
+
cacheTokenSize,
|
|
812
|
+
cachePrefixContentCount,
|
|
813
|
+
cachePrefixHash,
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
return cacheName;
|
|
817
|
+
} catch (err) {
|
|
818
|
+
process.stderr.write(`[gemini] cachedContents.create error: ${err?.message || err}\n`);
|
|
819
|
+
return canAttachState ? state.cacheName : null;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async send(messages, model, tools, sendOpts) {
|
|
824
|
+
try {
|
|
825
|
+
return await this._doSend(messages, model, tools, sendOpts);
|
|
826
|
+
} catch (err) {
|
|
827
|
+
if (err.message && (err.message.includes('401') || err.message.includes('403'))) {
|
|
828
|
+
process.stderr.write(`[provider] Auth error, re-reading config...\n`);
|
|
829
|
+
this.reloadApiKey();
|
|
830
|
+
return await this._doSend(messages, model, tools, sendOpts);
|
|
831
|
+
}
|
|
832
|
+
throw err;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async _doSend(messages, model, tools, sendOpts) {
|
|
837
|
+
const opts = sendOpts || {};
|
|
838
|
+
const signal = opts.signal || null;
|
|
839
|
+
if (signal?.aborted) {
|
|
840
|
+
const reason = signal.reason;
|
|
841
|
+
throw reason instanceof Error ? reason : new Error('Gemini request aborted by session close');
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const useModel = model || await ensureLatestGeminiModel(this);
|
|
845
|
+
const systemInstruction = messages
|
|
846
|
+
.filter(m => m.role === 'system')
|
|
847
|
+
.map(m => m.content)
|
|
848
|
+
.join('\n\n') || undefined;
|
|
849
|
+
const chatMsgs = messages.filter(m => m.role !== 'system');
|
|
850
|
+
const contents = toGeminiContents(chatMsgs);
|
|
851
|
+
if (!contents.length)
|
|
852
|
+
throw new Error('No messages to send');
|
|
853
|
+
|
|
854
|
+
const geminiTools = tools?.length ? [toGeminiTools(tools)] : undefined;
|
|
855
|
+
const toolConfig = geminiTools ? toGeminiToolConfig(opts.toolChoice) : undefined;
|
|
856
|
+
try { opts.onStageChange?.('requesting'); } catch {}
|
|
857
|
+
|
|
858
|
+
// Explicit cachedContents (system + tools + prior-turn transcript).
|
|
859
|
+
// Per Google docs, `tools` must be supplied on BOTH the cache create
|
|
860
|
+
// call AND every subsequent generate_content call — the cache stores
|
|
861
|
+
// the schema for prompt-token credit but the runtime model still
|
|
862
|
+
// needs the tool schema to actually emit function calls. Sending
|
|
863
|
+
// cachedContent without tools yields an empty completion (function
|
|
864
|
+
// calling silently disabled). The contents payload captures the
|
|
865
|
+
// accumulated prefix; we refresh the cache every N iterations so
|
|
866
|
+
// recent turns also enter the cached prefix instead of being billed
|
|
867
|
+
// at full input rates.
|
|
868
|
+
const cachedContent = await this._ensureGeminiCache({
|
|
869
|
+
apiKey: this._getApiKey(),
|
|
870
|
+
model: useModel,
|
|
871
|
+
systemInstruction,
|
|
872
|
+
geminiTools,
|
|
873
|
+
contents,
|
|
874
|
+
opts,
|
|
875
|
+
});
|
|
876
|
+
try { opts.onStageChange?.('requesting'); } catch {}
|
|
877
|
+
|
|
878
|
+
// When cachedContent is attached we bypass @google/generative-ai
|
|
879
|
+
// (deprecated; v1beta v1.x docs explicitly forbid re-sending tools or
|
|
880
|
+
// systemInstruction once a cache carries them, but the bundled SDK
|
|
881
|
+
// can't actually issue a tool-less generateContent call). REST direct
|
|
882
|
+
// sends the v1beta payload Google's new genai client uses, so the
|
|
883
|
+
// cache owns system/tools and the runtime gets a clean cache hit.
|
|
884
|
+
let response;
|
|
885
|
+
if (cachedContent) {
|
|
886
|
+
const apiKey = this._getApiKey();
|
|
887
|
+
const genUrl = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(useModel)}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
888
|
+
const cachedPrefixContentCount = Number.isFinite(Number(opts.providerState?.gemini?.cachePrefixContentCount))
|
|
889
|
+
? Math.max(0, Math.min(contents.length, Math.trunc(Number(opts.providerState.gemini.cachePrefixContentCount))))
|
|
890
|
+
: 0;
|
|
891
|
+
const deltaContents = contents.slice(cachedPrefixContentCount);
|
|
892
|
+
// Cache carries the recorded prefix. Send every uncached tail turn,
|
|
893
|
+
// not just the last message, so reused cachedContents preserve
|
|
894
|
+
// full conversation context between periodic refreshes.
|
|
895
|
+
const body = {
|
|
896
|
+
contents: deltaContents.length ? deltaContents : contents.slice(-1),
|
|
897
|
+
cachedContent,
|
|
898
|
+
};
|
|
899
|
+
if (toolConfig) body.toolConfig = toolConfig;
|
|
900
|
+
const fetchResult = await runGeminiOperationWithTimeout({
|
|
901
|
+
label: 'Gemini REST generateContent total',
|
|
902
|
+
timeoutMs: GEMINI_GENERATE_TOTAL_TIMEOUT_MS,
|
|
903
|
+
signal,
|
|
904
|
+
run: (totalSignal) => withRetry(
|
|
905
|
+
() => runGeminiOperationWithTimeout({
|
|
906
|
+
label: 'Gemini REST generateContent first byte',
|
|
907
|
+
timeoutMs: GEMINI_FIRST_BYTE_TIMEOUT_MS,
|
|
908
|
+
signal: totalSignal,
|
|
909
|
+
run: async (opSignal) => {
|
|
910
|
+
const res = await fetch(genUrl, {
|
|
911
|
+
method: 'POST',
|
|
912
|
+
headers: { 'Content-Type': 'application/json' },
|
|
913
|
+
body: JSON.stringify(body),
|
|
914
|
+
signal: opSignal,
|
|
915
|
+
dispatcher: getLlmDispatcher(),
|
|
916
|
+
});
|
|
917
|
+
if (!res.ok) {
|
|
918
|
+
const text = await res.text().catch(() => '');
|
|
919
|
+
const err = new Error(`Gemini REST generateContent ${res.status}: ${text.slice(0, 300)}`);
|
|
920
|
+
err.status = res.status;
|
|
921
|
+
throw err;
|
|
922
|
+
}
|
|
923
|
+
return await res.json();
|
|
924
|
+
},
|
|
925
|
+
}),
|
|
926
|
+
{
|
|
927
|
+
signal: totalSignal,
|
|
928
|
+
onRetry: ({ attempt, lastErr }) => {
|
|
929
|
+
try { opts.onStageChange?.('requesting'); } catch {}
|
|
930
|
+
process.stderr.write(`[gemini-rest] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}\n`);
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
),
|
|
934
|
+
});
|
|
935
|
+
response = fetchResult;
|
|
936
|
+
} else {
|
|
937
|
+
const genModel = this.genAI.getGenerativeModel({
|
|
938
|
+
model: useModel,
|
|
939
|
+
systemInstruction,
|
|
940
|
+
tools: geminiTools,
|
|
941
|
+
...(toolConfig ? { toolConfig } : {}),
|
|
942
|
+
});
|
|
943
|
+
const result = await runGeminiOperationWithTimeout({
|
|
944
|
+
label: 'Gemini generateContent total',
|
|
945
|
+
timeoutMs: GEMINI_GENERATE_TOTAL_TIMEOUT_MS,
|
|
946
|
+
signal,
|
|
947
|
+
run: (totalSignal) => withRetry(
|
|
948
|
+
() => runGeminiOperationWithTimeout({
|
|
949
|
+
label: 'Gemini generateContent first byte',
|
|
950
|
+
timeoutMs: GEMINI_FIRST_BYTE_TIMEOUT_MS,
|
|
951
|
+
signal: totalSignal,
|
|
952
|
+
run: (opSignal) => genModel.generateContent({ contents }, { signal: opSignal }),
|
|
953
|
+
}),
|
|
954
|
+
{
|
|
955
|
+
signal: totalSignal,
|
|
956
|
+
onRetry: ({ attempt, lastErr }) => {
|
|
957
|
+
try { opts.onStageChange?.('requesting'); } catch {}
|
|
958
|
+
process.stderr.write(`[gemini] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}\n`);
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
),
|
|
962
|
+
});
|
|
963
|
+
response = result.response;
|
|
964
|
+
}
|
|
965
|
+
writeGeminiCacheTrace({
|
|
966
|
+
opts,
|
|
967
|
+
model: useModel,
|
|
968
|
+
systemInstruction,
|
|
969
|
+
tools,
|
|
970
|
+
contents,
|
|
971
|
+
usageMetadata: response.usageMetadata,
|
|
972
|
+
cachedContent,
|
|
973
|
+
});
|
|
974
|
+
const candidate = response.candidates?.[0] || null;
|
|
975
|
+
const textParts = candidate?.content?.parts?.filter(p => 'text' in p) ?? [];
|
|
976
|
+
const content = textParts.map(p => 'text' in p ? p.text : '').join('');
|
|
977
|
+
const toolCalls = parseToolCalls(candidate?.content?.parts ?? []);
|
|
978
|
+
// Inspect candidate.finishReason — Gemini reports terminal status here.
|
|
979
|
+
// Only STOP (and the legacy "FINISH_REASON_STOP") plus tool/function-
|
|
980
|
+
// call paths represent a fully delivered turn. MAX_TOKENS / SAFETY /
|
|
981
|
+
// RECITATION / OTHER all mean the candidate was cut off before the
|
|
982
|
+
// model finished, and surfacing the partial text as final would
|
|
983
|
+
// silently accept a truncated answer. Convert those into a typed
|
|
984
|
+
// provider-incomplete error so the loop can decide whether to retry,
|
|
985
|
+
// nudge, or surface to the user. Missing finishReason (still
|
|
986
|
+
// streaming / unknown) is left alone — existing success paths for
|
|
987
|
+
// genuinely complete responses keep working.
|
|
988
|
+
const finishReason = candidate?.finishReason || null;
|
|
989
|
+
const incompleteFinishReasons = new Set([
|
|
990
|
+
'MAX_TOKENS',
|
|
991
|
+
'SAFETY',
|
|
992
|
+
'RECITATION',
|
|
993
|
+
'OTHER',
|
|
994
|
+
'BLOCKLIST',
|
|
995
|
+
'PROHIBITED_CONTENT',
|
|
996
|
+
'SPII',
|
|
997
|
+
'MALFORMED_FUNCTION_CALL',
|
|
998
|
+
]);
|
|
999
|
+
if (finishReason && incompleteFinishReasons.has(finishReason)) {
|
|
1000
|
+
const err = Object.assign(
|
|
1001
|
+
new Error(`Gemini response incomplete: finishReason=${finishReason}`),
|
|
1002
|
+
{
|
|
1003
|
+
name: 'ProviderIncompleteError',
|
|
1004
|
+
code: 'PROVIDER_INCOMPLETE',
|
|
1005
|
+
providerIncomplete: true,
|
|
1006
|
+
finishReason,
|
|
1007
|
+
partialContent: content,
|
|
1008
|
+
partialToolCalls: toolCalls,
|
|
1009
|
+
model: useModel,
|
|
1010
|
+
rawUsage: response.usageMetadata || null,
|
|
1011
|
+
},
|
|
1012
|
+
);
|
|
1013
|
+
throw err;
|
|
1014
|
+
}
|
|
1015
|
+
const um = response.usageMetadata || null;
|
|
1016
|
+
// Hoist cachedTokens so the returned usage block can reuse the
|
|
1017
|
+
// exact value the trace already recorded (including the
|
|
1018
|
+
// cachedFallback when cachedContentTokenCount under-reports).
|
|
1019
|
+
let cachedTokens = 0;
|
|
1020
|
+
if (um) {
|
|
1021
|
+
const {
|
|
1022
|
+
inputTokens,
|
|
1023
|
+
reportedCachedTokens,
|
|
1024
|
+
cachedFallbackTokens,
|
|
1025
|
+
cachedTokens: resolvedCachedTokens,
|
|
1026
|
+
cacheTokenSource,
|
|
1027
|
+
} = _resolveGeminiCacheUsage({
|
|
1028
|
+
usageMetadata: um,
|
|
1029
|
+
cachedContent,
|
|
1030
|
+
providerState: opts.providerState,
|
|
1031
|
+
});
|
|
1032
|
+
cachedTokens = resolvedCachedTokens;
|
|
1033
|
+
const outputTokens = (um.candidatesTokenCount || 0) + (um.thoughtsTokenCount || 0);
|
|
1034
|
+
if (cachedContent && inputTokens > 0 && cachedTokens <= 0) {
|
|
1035
|
+
try {
|
|
1036
|
+
appendBridgeTrace({
|
|
1037
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
1038
|
+
iteration: Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null,
|
|
1039
|
+
kind: 'gemini_cache_anomaly',
|
|
1040
|
+
payload: {
|
|
1041
|
+
reason: 'cached_content_attached_but_zero_cached_tokens',
|
|
1042
|
+
inputTokens,
|
|
1043
|
+
reportedCachedTokens,
|
|
1044
|
+
cachedFallbackTokens,
|
|
1045
|
+
cacheTokenSource,
|
|
1046
|
+
cacheName: opts.providerState?.gemini?.cacheName || null,
|
|
1047
|
+
cachePrefixContentCount: opts.providerState?.gemini?.cachePrefixContentCount ?? null,
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
} catch {}
|
|
1051
|
+
}
|
|
1052
|
+
traceBridgeUsage({
|
|
1053
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
1054
|
+
iteration: Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null,
|
|
1055
|
+
inputTokens,
|
|
1056
|
+
outputTokens,
|
|
1057
|
+
cachedTokens,
|
|
1058
|
+
cacheWriteTokens: 0,
|
|
1059
|
+
promptTokens: inputTokens,
|
|
1060
|
+
model: useModel,
|
|
1061
|
+
modelDisplay: useModel,
|
|
1062
|
+
rawUsage: um,
|
|
1063
|
+
provider: 'gemini',
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
content,
|
|
1068
|
+
model: useModel,
|
|
1069
|
+
toolCalls,
|
|
1070
|
+
providerState: opts.providerState,
|
|
1071
|
+
usage: um ? (() => {
|
|
1072
|
+
const input = um.promptTokenCount || um.totalTokenCount || 0;
|
|
1073
|
+
return {
|
|
1074
|
+
inputTokens: input,
|
|
1075
|
+
outputTokens: (um.candidatesTokenCount || 0) + (um.thoughtsTokenCount || 0),
|
|
1076
|
+
// Use the already-computed cachedTokens (with
|
|
1077
|
+
// cache-create fallback applied) rather than the raw
|
|
1078
|
+
// metadata field, so the returned usage matches what
|
|
1079
|
+
// traceBridgeUsage recorded for this same call.
|
|
1080
|
+
cachedTokens,
|
|
1081
|
+
// Gemini promptTokenCount is total (cachedContentTokenCount
|
|
1082
|
+
// is a subset). Alias directly into promptTokens.
|
|
1083
|
+
promptTokens: input,
|
|
1084
|
+
};
|
|
1085
|
+
})() : undefined,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async listModels() {
|
|
1090
|
+
const cached = _loadModelCache();
|
|
1091
|
+
if (cached) return cached;
|
|
1092
|
+
// Dynamic lookup via Gemini v1beta /models. Requires API key.
|
|
1093
|
+
const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
|
|
1094
|
+
if (!apiKey) return MODELS; // no key — return minimal static list
|
|
1095
|
+
try {
|
|
1096
|
+
return await this._fetchAndCacheModels(apiKey);
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
process.stderr.write(`[gemini] listModels fetch failed (${err.message})\n`);
|
|
1099
|
+
return MODELS;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Shared fetch+normalize+enrich+write used by both listModels() (after the
|
|
1104
|
+
// TTL check) and _refreshModelCache() (bypassing it). Throws on failure so
|
|
1105
|
+
// each caller applies its own fallback/logging.
|
|
1106
|
+
async _fetchAndCacheModels(apiKey) {
|
|
1107
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`;
|
|
1108
|
+
const listSignal = AbortSignal.timeout(60_000);
|
|
1109
|
+
const res = await fetch(url, { signal: listSignal, dispatcher: getLlmDispatcher() });
|
|
1110
|
+
if (!res.ok) throw new Error(`gemini list_models ${res.status}`);
|
|
1111
|
+
const data = await res.json();
|
|
1112
|
+
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1113
|
+
// Filter to Gemini family; skip embedding/imagen endpoints.
|
|
1114
|
+
const normalized = items
|
|
1115
|
+
.filter(m => (m?.name || '').includes('gemini'))
|
|
1116
|
+
.filter(m => !/embedding|aqa|imagen/.test(m?.name || ''))
|
|
1117
|
+
.map(m => {
|
|
1118
|
+
const id = (m.name || '').replace(/^models\//, '');
|
|
1119
|
+
const family = /flash-lite/.test(id) ? 'gemini-flash-lite'
|
|
1120
|
+
: /flash/.test(id) ? 'gemini-flash'
|
|
1121
|
+
: /pro/.test(id) ? 'gemini-pro'
|
|
1122
|
+
: 'gemini';
|
|
1123
|
+
return {
|
|
1124
|
+
id,
|
|
1125
|
+
display: m.displayName || id,
|
|
1126
|
+
family,
|
|
1127
|
+
provider: 'gemini',
|
|
1128
|
+
contextWindow: m.inputTokenLimit || 1000000,
|
|
1129
|
+
outputTokens: m.outputTokenLimit || 8192,
|
|
1130
|
+
tier: 'version',
|
|
1131
|
+
latest: false,
|
|
1132
|
+
description: m.description || '',
|
|
1133
|
+
};
|
|
1134
|
+
});
|
|
1135
|
+
_markLatestGemini(normalized);
|
|
1136
|
+
// LiteLLM catalog overlays pricing and updated metadata.
|
|
1137
|
+
const { enrichModels } = await import('./model-catalog.mjs');
|
|
1138
|
+
const enriched = await enrichModels(normalized);
|
|
1139
|
+
_saveModelCache(enriched);
|
|
1140
|
+
return enriched;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Force a catalog refresh (ignores the 24h disk TTL). De-duped via
|
|
1144
|
+
// _modelRefreshInFlight so concurrent callers share one HTTP round-trip.
|
|
1145
|
+
// Fire-and-forget context: failures are caught/logged, returning null.
|
|
1146
|
+
async _refreshModelCache() {
|
|
1147
|
+
if (_modelRefreshInFlight) return _modelRefreshInFlight;
|
|
1148
|
+
_modelRefreshInFlight = (async () => {
|
|
1149
|
+
try {
|
|
1150
|
+
const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
|
|
1151
|
+
if (!apiKey) return null; // no key — nothing to refresh
|
|
1152
|
+
const enriched = await this._fetchAndCacheModels(apiKey);
|
|
1153
|
+
process.stderr.write(`[gemini] catalog refreshed (${enriched.length} models)\n`);
|
|
1154
|
+
return enriched;
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
process.stderr.write(`[gemini] catalog refresh failed (${err.message})\n`);
|
|
1157
|
+
return null;
|
|
1158
|
+
} finally {
|
|
1159
|
+
_modelRefreshInFlight = null;
|
|
1160
|
+
}
|
|
1161
|
+
})();
|
|
1162
|
+
return _modelRefreshInFlight;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async isAvailable() {
|
|
1166
|
+
try {
|
|
1167
|
+
const model = this.genAI.getGenerativeModel({ model: DEFAULT_MODEL });
|
|
1168
|
+
await model.generateContent('hi');
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|