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