mixdog 0.9.63 → 0.9.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/turn-snapshot.mjs +184 -0
- 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/runtime-core.mjs +4 -0
- package/src/session-runtime/session-text.mjs +4 -4
- package/src/session-runtime/session-turn-api.mjs +7 -0
- 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.jsx +1 -1
- 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 +187 -99
- package/src/tui/engine/agent-envelope.mjs +5 -5
- package/src/tui/engine/live-share.mjs +100 -0
- 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 +6 -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 +29 -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,3063 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Regression tests pinning the cross-provider "native tool_call extraction"
|
|
3
|
-
// contract: when a provider's native parser is fed a well-formed tool_call
|
|
4
|
-
// payload, it MUST surface the call in our canonical toolCalls shape
|
|
5
|
-
// ({ id, name, arguments }). Synthetic inputs fed directly to the exported parser, asserting the
|
|
6
|
-
// resulting outcome. No network, no model. Each provider also gets one
|
|
7
|
-
// negative case (no native tool_call → undefined / empty).
|
|
8
|
-
//
|
|
9
|
-
// Parser entry points (file:line at authoring time) and sharing notes are
|
|
10
|
-
// documented inline per provider block below.
|
|
11
|
-
import test from 'node:test';
|
|
12
|
-
import assert from 'node:assert/strict';
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
14
|
-
import { parse } from 'acorn';
|
|
15
|
-
import { analyze } from 'eslint-scope';
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
OpenAICompatProvider,
|
|
19
|
-
_toResponsesToolsForTest,
|
|
20
|
-
_toXaiResponsesInputForTest,
|
|
21
|
-
parseToolCalls as compatParseToolCalls,
|
|
22
|
-
parseResponsesToolCalls as compatParseResponsesToolCalls,
|
|
23
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-compat.mjs';
|
|
24
|
-
import {
|
|
25
|
-
GrokOAuthProvider,
|
|
26
|
-
} from '../src/runtime/agent/orchestrator/providers/grok-oauth.mjs';
|
|
27
|
-
import {
|
|
28
|
-
consumeCompatResponsesStream,
|
|
29
|
-
isInvalidToolArgsMarker,
|
|
30
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
|
|
31
|
-
import {
|
|
32
|
-
consumeCompatChatCompletionStream,
|
|
33
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
|
|
34
|
-
import {
|
|
35
|
-
_computeDelta,
|
|
36
|
-
_buildResponseCreateFrame,
|
|
37
|
-
_sansInput,
|
|
38
|
-
_stableStringify,
|
|
39
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs';
|
|
40
|
-
import {
|
|
41
|
-
_cacheObservationForTest,
|
|
42
|
-
_cacheContinuityResetReasonForTest,
|
|
43
|
-
sendViaWebSocket,
|
|
44
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
45
|
-
import {
|
|
46
|
-
_withCodexWsClientMetadata,
|
|
47
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
48
|
-
import {
|
|
49
|
-
_captureTurnStateFromEvent,
|
|
50
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs';
|
|
51
|
-
import {
|
|
52
|
-
createGeminiTextLeakGuard,
|
|
53
|
-
parseToolCalls as geminiParseToolCalls,
|
|
54
|
-
} from '../src/runtime/agent/orchestrator/providers/gemini.mjs';
|
|
55
|
-
import {
|
|
56
|
-
_resolveGeminiCacheUsage,
|
|
57
|
-
} from '../src/runtime/agent/orchestrator/providers/gemini-cache.mjs';
|
|
58
|
-
import { parseSSEStream as anthropicParseSSEStream } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
59
|
-
import { _buildRequestBodyForCacheSmoke } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
60
|
-
import { _test as _anthropicApiKeyTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
61
|
-
import { _toAnthropicMessagesForTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
62
|
-
import { _test as _anthropicOAuthTest } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
63
|
-
import {
|
|
64
|
-
EFFORT_BETA_HEADER,
|
|
65
|
-
LEGACY_EFFORT_BUDGET,
|
|
66
|
-
modelSupportsEffort,
|
|
67
|
-
modelSupportsMaxEffort,
|
|
68
|
-
normalizeAnthropicEffortInput,
|
|
69
|
-
shouldIncludeEffortBeta,
|
|
70
|
-
} from '../src/runtime/agent/orchestrator/providers/anthropic-effort.mjs';
|
|
71
|
-
import { buildAnthropicBetaHeaders } from '../src/runtime/agent/orchestrator/providers/anthropic-betas.mjs';
|
|
72
|
-
import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
|
|
73
|
-
import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
|
|
74
|
-
import {
|
|
75
|
-
OpenAIOAuthProvider,
|
|
76
|
-
buildRequestBody as buildOpenAIOAuthRequestBody,
|
|
77
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
|
|
78
|
-
import { _convertMessagesToResponsesInputForTest } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
|
|
79
|
-
import { OpenAIDirectProvider } from '../src/runtime/agent/orchestrator/providers/openai-ws.mjs';
|
|
80
|
-
|
|
81
|
-
// --- Helpers ---------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
test('OpenAI API-key request keeps public defaults without network under every transport mode', async (t) => {
|
|
84
|
-
const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
85
|
-
t.after(() => {
|
|
86
|
-
if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
87
|
-
else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
|
|
88
|
-
});
|
|
89
|
-
const provider = new OpenAIDirectProvider({ apiKey: 'fixture-openai-key' });
|
|
90
|
-
for (const mode of ['auto', 'ws-full', 'ws-delta', 'http-sse']) {
|
|
91
|
-
process.env.MIXDOG_OAI_TRANSPORT = mode;
|
|
92
|
-
const calls = [];
|
|
93
|
-
let captured = null;
|
|
94
|
-
const result = await provider.send(
|
|
95
|
-
[{ role: 'user', content: 'fixture' }],
|
|
96
|
-
'gpt-5.4',
|
|
97
|
-
[],
|
|
98
|
-
{
|
|
99
|
-
sessionId: `direct-request-defaults-${mode}`,
|
|
100
|
-
_fetchFn: async () => { throw new Error('global fetch seam must not run'); },
|
|
101
|
-
_sendViaWebSocketFn: async (request) => {
|
|
102
|
-
calls.push('ws');
|
|
103
|
-
captured = request;
|
|
104
|
-
return { content: 'ws-ok', toolCalls: [] };
|
|
105
|
-
},
|
|
106
|
-
_sendViaHttpSseFn: async (request) => {
|
|
107
|
-
calls.push('http');
|
|
108
|
-
captured = request;
|
|
109
|
-
return { content: 'http-ok', toolCalls: [] };
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
assert.deepEqual(calls, mode === 'http-sse' ? ['http'] : ['ws'], mode);
|
|
114
|
-
assert.equal(result.content, mode === 'http-sse' ? 'http-ok' : 'ws-ok');
|
|
115
|
-
assert.equal(captured.auth.type, 'openai-direct');
|
|
116
|
-
assert.equal(captured.auth.apiKey, 'fixture-openai-key');
|
|
117
|
-
if (mode !== 'http-sse') assert.equal(captured.traceProvider, 'openai-direct');
|
|
118
|
-
assert.equal(captured.body.store, true);
|
|
119
|
-
assert.equal(captured.body.prompt_cache_retention, '24h');
|
|
120
|
-
assert.equal(captured.body.stream, true);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
function directHandshakeError(status) {
|
|
125
|
-
return Object.assign(new Error(`handshake ${status}`), { httpStatus: status });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function directWsEntry() {
|
|
129
|
-
return { socket: { close() {} }, ephemeral: true };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
test('OpenAI API-key unsupported handshake 403/404/429 falls back once without nested WS retries', async (t) => {
|
|
133
|
-
const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
134
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'auto';
|
|
135
|
-
t.after(() => {
|
|
136
|
-
if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
137
|
-
else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
|
|
138
|
-
});
|
|
139
|
-
for (const status of [403, 404, 429]) {
|
|
140
|
-
const provider = new OpenAIDirectProvider({ apiKey: 'fixture-openai-key' });
|
|
141
|
-
let acquires = 0;
|
|
142
|
-
let httpCalls = 0;
|
|
143
|
-
const result = await provider.send([], 'gpt-5.4', [], {
|
|
144
|
-
_fetchFn: async () => { throw new Error('global fetch seam must not run'); },
|
|
145
|
-
_webSocketTestSeams: {
|
|
146
|
-
// Neither null nor an inverted caller policy may override the
|
|
147
|
-
// direct provider's mandatory handshake policy.
|
|
148
|
-
handshakeErrorPolicy: status === 403
|
|
149
|
-
? null
|
|
150
|
-
: () => ({ retry: true, httpFallback: false }),
|
|
151
|
-
_acquireWithRetryFn: async () => {
|
|
152
|
-
acquires += 1;
|
|
153
|
-
throw directHandshakeError(status);
|
|
154
|
-
},
|
|
155
|
-
_sleepFn: async () => {},
|
|
156
|
-
_sendSpanTraceFn: () => {},
|
|
157
|
-
_agentTraceFn: () => {},
|
|
158
|
-
},
|
|
159
|
-
_sendViaHttpSseFn: async () => {
|
|
160
|
-
httpCalls += 1;
|
|
161
|
-
return { content: `http-${status}`, toolCalls: [] };
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
assert.equal(result.content, `http-${status}`);
|
|
165
|
-
assert.equal(acquires, 1, `handshake ${status} must not retry WS`);
|
|
166
|
-
assert.equal(httpCalls, 1, `handshake ${status} gets one HTTP fallback`);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('OpenAI API-key every application 4xx and exposed output never replay or fall back', async (t) => {
|
|
171
|
-
const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
172
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'auto';
|
|
173
|
-
t.after(() => {
|
|
174
|
-
if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
175
|
-
else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
|
|
176
|
-
});
|
|
177
|
-
const cases = [
|
|
178
|
-
...[400, 401, 402, 403, 404, 409, 418, 422, 429, 451, 499]
|
|
179
|
-
.map((status) => ({ status, exposed: null })),
|
|
180
|
-
{ status: 500, exposed: 'text' },
|
|
181
|
-
{ status: 500, exposed: 'tool' },
|
|
182
|
-
];
|
|
183
|
-
for (const { status, exposed } of cases) {
|
|
184
|
-
const provider = new OpenAIDirectProvider({ apiKey: 'fixture-openai-key' });
|
|
185
|
-
let acquires = 0;
|
|
186
|
-
let streams = 0;
|
|
187
|
-
let httpCalls = 0;
|
|
188
|
-
let reloads = 0;
|
|
189
|
-
let visibleTextDeltas = 0;
|
|
190
|
-
let dispatchedToolCalls = 0;
|
|
191
|
-
provider.reloadApiKey = () => {
|
|
192
|
-
reloads += 1;
|
|
193
|
-
return 'replacement-key';
|
|
194
|
-
};
|
|
195
|
-
await assert.rejects(provider.send([], 'gpt-5.4', [], {
|
|
196
|
-
onTextDelta: () => { visibleTextDeltas += 1; },
|
|
197
|
-
onToolCall: () => { dispatchedToolCalls += 1; },
|
|
198
|
-
_fetchFn: async () => { throw new Error('global fetch seam must not run'); },
|
|
199
|
-
_webSocketTestSeams: {
|
|
200
|
-
_acquireWithRetryFn: async () => {
|
|
201
|
-
acquires += 1;
|
|
202
|
-
return { entry: directWsEntry(), reused: false };
|
|
203
|
-
},
|
|
204
|
-
_sendFrameFn: async () => {},
|
|
205
|
-
_streamFn: async ({ state, onTextDelta, onToolCall }) => {
|
|
206
|
-
streams += 1;
|
|
207
|
-
if (exposed === 'text') {
|
|
208
|
-
onTextDelta?.('visible-once');
|
|
209
|
-
state.emittedText = true;
|
|
210
|
-
}
|
|
211
|
-
if (exposed === 'tool') {
|
|
212
|
-
onToolCall?.({ id: 'tool-once', name: 'read', arguments: {} });
|
|
213
|
-
state.emittedToolCall = true;
|
|
214
|
-
}
|
|
215
|
-
throw Object.assign(new Error(`application ${status} ${exposed || ''}`), { httpStatus: status });
|
|
216
|
-
},
|
|
217
|
-
_sleepFn: async () => {},
|
|
218
|
-
_sendSpanTraceFn: () => {},
|
|
219
|
-
_agentTraceFn: () => {},
|
|
220
|
-
},
|
|
221
|
-
_sendViaHttpSseFn: async () => {
|
|
222
|
-
httpCalls += 1;
|
|
223
|
-
throw new Error('application/visible output must not reach HTTP');
|
|
224
|
-
},
|
|
225
|
-
}), new RegExp(`application ${status}`));
|
|
226
|
-
assert.equal(acquires, 1);
|
|
227
|
-
assert.equal(streams, 1);
|
|
228
|
-
assert.equal(httpCalls, 0);
|
|
229
|
-
assert.equal(reloads, 0);
|
|
230
|
-
assert.equal(visibleTextDeltas, exposed === 'text' ? 1 : 0);
|
|
231
|
-
assert.equal(dispatchedToolCalls, exposed === 'tool' ? 1 : 0);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test('Anthropic API-key gates deferred beta to requests that carry deferred tools', async () => {
|
|
236
|
-
const provider = Object.create((await import('../src/runtime/agent/orchestrator/providers/anthropic.mjs')).AnthropicProvider.prototype);
|
|
237
|
-
provider.name = 'anthropic';
|
|
238
|
-
provider.config = {};
|
|
239
|
-
provider.fastModeBetaHeaderLatched = false;
|
|
240
|
-
const calls = [];
|
|
241
|
-
provider.client = { messages: { create(params, options) {
|
|
242
|
-
calls.push({ params, options });
|
|
243
|
-
return { asResponse: async () => ({
|
|
244
|
-
...anthropicSseResponse([
|
|
245
|
-
{ type: 'message_start', message: { model: params.model, usage: { input_tokens: 1 } } },
|
|
246
|
-
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
247
|
-
{ type: 'message_stop' },
|
|
248
|
-
]),
|
|
249
|
-
ok: true,
|
|
250
|
-
status: 200,
|
|
251
|
-
headers: new Map(),
|
|
252
|
-
}) };
|
|
253
|
-
} } };
|
|
254
|
-
|
|
255
|
-
await provider._doSend([{ role: 'user', content: 'plain' }], 'claude-sonnet-4-6', [], {});
|
|
256
|
-
assert.doesNotMatch(calls[0].options.headers['anthropic-beta'], /advanced-tool-use/);
|
|
257
|
-
|
|
258
|
-
const active = [{ name: 'load_tool', description: 'load', inputSchema: { type: 'object', properties: {} } }];
|
|
259
|
-
const deferred = { name: 'mcp__demo__ping', description: 'ping', inputSchema: { type: 'object', properties: {} } };
|
|
260
|
-
await provider._doSend([{ role: 'user', content: 'load' }], 'claude-sonnet-4-6', active, {
|
|
261
|
-
session: {
|
|
262
|
-
deferredNativeTools: true,
|
|
263
|
-
deferredDiscoveredTools: [deferred.name],
|
|
264
|
-
deferredToolCatalog: [...active, deferred],
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
assert.match(calls[1].options.headers['anthropic-beta'], /advanced-tool-use/);
|
|
268
|
-
assert.equal(calls[1].params.tools.find((tool) => tool.name === deferred.name)?.defer_loading, true);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test('native deferred history normalizes per provider without leaking OpenAI variants to xAI', () => {
|
|
272
|
-
const nativePayload = {
|
|
273
|
-
provider: 'openai-oauth',
|
|
274
|
-
toolReferences: ['mcp__demo__ping'],
|
|
275
|
-
openaiTools: [{
|
|
276
|
-
type: 'function',
|
|
277
|
-
name: 'mcp__demo__ping',
|
|
278
|
-
defer_loading: true,
|
|
279
|
-
parameters: { type: 'object', properties: {} },
|
|
280
|
-
}],
|
|
281
|
-
};
|
|
282
|
-
const history = [
|
|
283
|
-
{ role: 'user', content: 'load it' },
|
|
284
|
-
{
|
|
285
|
-
role: 'assistant',
|
|
286
|
-
content: '',
|
|
287
|
-
toolCalls: [{ id: 'load-1', name: 'load_tool', arguments: { names: ['mcp__demo__ping'] }, nativeType: 'tool_search_call' }],
|
|
288
|
-
},
|
|
289
|
-
{ role: 'tool', toolCallId: 'load-1', content: 'Loaded', nativeToolSearch: nativePayload },
|
|
290
|
-
];
|
|
291
|
-
const openai = _convertMessagesToResponsesInputForTest(history);
|
|
292
|
-
assert.equal(openai[1].type, 'tool_search_call');
|
|
293
|
-
assert.equal(openai[2].type, 'tool_search_output');
|
|
294
|
-
assert.equal(openai[2].tools[0].name, 'mcp__demo__ping');
|
|
295
|
-
|
|
296
|
-
const xai = _toXaiResponsesInputForTest(history, {
|
|
297
|
-
xaiResponses: { previousResponseId: 'resp-1', seenMessageCount: 0, model: 'grok-4' },
|
|
298
|
-
}, { model: 'grok-4' }).input;
|
|
299
|
-
assert.equal(xai.some((item) => item.type === 'tool_search_call' || item.type === 'tool_search_output'), false);
|
|
300
|
-
assert.equal(xai[1].type, 'function_call');
|
|
301
|
-
assert.equal(xai[2].type, 'function_call_output');
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test('Anthropic native deferred result retains tool_reference history and defer_loading declarations', () => {
|
|
305
|
-
const base = [{ name: 'load_tool', description: 'loader', inputSchema: { type: 'object', properties: {} } }];
|
|
306
|
-
const deferred = { name: 'mcp__demo__ping', description: 'ping', inputSchema: { type: 'object', properties: {} } };
|
|
307
|
-
const session = {
|
|
308
|
-
deferredNativeTools: true,
|
|
309
|
-
deferredToolCatalog: [...base, deferred],
|
|
310
|
-
};
|
|
311
|
-
const firstBody = _buildRequestBodyForCacheSmoke(
|
|
312
|
-
[{ role: 'user', content: 'find ping' }],
|
|
313
|
-
'claude-sonnet-4-6',
|
|
314
|
-
base,
|
|
315
|
-
{ session },
|
|
316
|
-
);
|
|
317
|
-
assert.equal(firstBody.tools.some((tool) => tool.name === 'mcp__demo__ping'), false);
|
|
318
|
-
assert.deepEqual(_anthropicApiKeyTest.deferredAnthropicTools(base, [], { session }), []);
|
|
319
|
-
const history = [
|
|
320
|
-
{
|
|
321
|
-
role: 'assistant',
|
|
322
|
-
content: '',
|
|
323
|
-
toolCalls: [{ id: 'load-a1', name: 'load_tool', arguments: { names: ['mcp__demo__ping'] } }],
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
role: 'tool',
|
|
327
|
-
toolCallId: 'load-a1',
|
|
328
|
-
content: 'Loaded',
|
|
329
|
-
nativeToolSearch: {
|
|
330
|
-
provider: 'anthropic-oauth',
|
|
331
|
-
toolReferences: ['mcp__demo__ping'],
|
|
332
|
-
openaiTools: [],
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
];
|
|
336
|
-
const body = _buildRequestBodyForCacheSmoke(history, 'claude-sonnet-4-6', base, { session });
|
|
337
|
-
const result = body.messages.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
338
|
-
.find((block) => block.type === 'tool_result');
|
|
339
|
-
assert.deepEqual(result.content, [{ type: 'tool_reference', tool_name: 'mcp__demo__ping' }]);
|
|
340
|
-
assert.equal(body.tools.find((tool) => tool.name === 'mcp__demo__ping')?.defer_loading, true);
|
|
341
|
-
const apiKeyHistory = history.map((message) => (
|
|
342
|
-
message.nativeToolSearch
|
|
343
|
-
? { ...message, nativeToolSearch: { ...message.nativeToolSearch, provider: 'anthropic' } }
|
|
344
|
-
: message
|
|
345
|
-
));
|
|
346
|
-
const apiKeyDiscovered = _anthropicApiKeyTest.deferredAnthropicTools(base, apiKeyHistory, { session });
|
|
347
|
-
assert.equal(apiKeyDiscovered.find((tool) => tool.name === 'mcp__demo__ping')?.deferLoading, true);
|
|
348
|
-
session.deferredDiscoveredTools = ['mcp__demo__ping'];
|
|
349
|
-
const compacted = _buildRequestBodyForCacheSmoke(
|
|
350
|
-
[{ role: 'user', content: 'continue after compact' }],
|
|
351
|
-
'claude-sonnet-4-6',
|
|
352
|
-
base,
|
|
353
|
-
{ session },
|
|
354
|
-
);
|
|
355
|
-
assert.equal(compacted.tools.find((tool) => tool.name === 'mcp__demo__ping')?.defer_loading, true);
|
|
356
|
-
const apiKeyCompacted = _anthropicApiKeyTest.deferredAnthropicTools(base, [], { session });
|
|
357
|
-
assert.equal(apiKeyCompacted.find((tool) => tool.name === 'mcp__demo__ping')?.deferLoading, true);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test('Anthropic API-key and OAuth preserve root properties across compound schemas', () => {
|
|
361
|
-
const properties = {
|
|
362
|
-
pattern: { type: 'string' },
|
|
363
|
-
path: { type: 'string' },
|
|
364
|
-
};
|
|
365
|
-
const branches = [
|
|
366
|
-
{ required: ['pattern'] },
|
|
367
|
-
{ required: ['path'] },
|
|
368
|
-
];
|
|
369
|
-
for (const compoundKey of ['oneOf', 'anyOf', 'allOf']) {
|
|
370
|
-
const schema = { type: 'object', properties, [compoundKey]: branches };
|
|
371
|
-
const expected = { type: 'object', properties };
|
|
372
|
-
const apiKey = _anthropicApiKeyTest.sanitizeInputSchema(schema, 'grep');
|
|
373
|
-
const oauth = _anthropicOAuthTest.sanitizeInputSchema(schema, 'grep');
|
|
374
|
-
|
|
375
|
-
assert.deepEqual(apiKey, expected, `API-key ${compoundKey}`);
|
|
376
|
-
assert.deepEqual(oauth, expected, `OAuth ${compoundKey}`);
|
|
377
|
-
assert.deepEqual(apiKey, oauth, `${compoundKey} parity`);
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test('OpenAI native provider-tag switches keep tool_search call/output paired', () => {
|
|
382
|
-
const history = [
|
|
383
|
-
{
|
|
384
|
-
role: 'assistant',
|
|
385
|
-
content: '',
|
|
386
|
-
toolCalls: [{ id: 'native-pair', name: 'load_tool', arguments: {}, nativeType: 'tool_search_call' }],
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
role: 'tool',
|
|
390
|
-
toolCallId: 'native-pair',
|
|
391
|
-
content: 'Loaded',
|
|
392
|
-
nativeToolSearch: {
|
|
393
|
-
provider: 'openai',
|
|
394
|
-
toolReferences: ['read'],
|
|
395
|
-
openaiTools: [{ type: 'function', name: 'read', parameters: { type: 'object', properties: {} } }],
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
];
|
|
399
|
-
const oauth = _convertMessagesToResponsesInputForTest(history, { nativeToolSearchProvider: 'openai-oauth' });
|
|
400
|
-
assert.deepEqual(oauth.map((item) => item.type), ['tool_search_call', 'tool_search_output']);
|
|
401
|
-
history[1].nativeToolSearch.provider = 'openai-oauth';
|
|
402
|
-
const direct = _convertMessagesToResponsesInputForTest(history, { nativeToolSearchProvider: 'openai' });
|
|
403
|
-
assert.deepEqual(direct.map((item) => item.type), ['tool_search_call', 'tool_search_output']);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
test('Anthropic native provider-tag switches preserve tool_reference and loaded schema bidirectionally', () => {
|
|
407
|
-
const base = [{ name: 'load_tool', description: 'loader', inputSchema: { type: 'object', properties: {} } }];
|
|
408
|
-
const deferred = { name: 'mcp__demo__ping', description: 'ping', inputSchema: { type: 'object', properties: {} } };
|
|
409
|
-
const session = {
|
|
410
|
-
deferredNativeTools: true,
|
|
411
|
-
deferredToolCatalog: [...base, deferred],
|
|
412
|
-
};
|
|
413
|
-
const history = (provider) => [
|
|
414
|
-
{
|
|
415
|
-
role: 'assistant',
|
|
416
|
-
content: '',
|
|
417
|
-
toolCalls: [{ id: 'anthropic-family-pair', name: 'load_tool', arguments: { names: ['mcp__demo__ping'] } }],
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
role: 'tool',
|
|
421
|
-
toolCallId: 'anthropic-family-pair',
|
|
422
|
-
content: 'Loaded deferred tools: mcp__demo__ping',
|
|
423
|
-
nativeToolSearch: {
|
|
424
|
-
provider,
|
|
425
|
-
toolReferences: ['mcp__demo__ping'],
|
|
426
|
-
openaiTools: [],
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
];
|
|
430
|
-
const apiKeyToOauth = _buildRequestBodyForCacheSmoke(
|
|
431
|
-
history('anthropic'),
|
|
432
|
-
'claude-sonnet-4-6',
|
|
433
|
-
base,
|
|
434
|
-
{ session },
|
|
435
|
-
);
|
|
436
|
-
const oauthResult = apiKeyToOauth.messages
|
|
437
|
-
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
438
|
-
.find((block) => block.type === 'tool_result');
|
|
439
|
-
assert.deepEqual(oauthResult.content, [{ type: 'tool_reference', tool_name: 'mcp__demo__ping' }]);
|
|
440
|
-
assert.equal(apiKeyToOauth.tools.find((tool) => tool.name === 'mcp__demo__ping')?.defer_loading, true);
|
|
441
|
-
|
|
442
|
-
const oauthHistory = history('anthropic-oauth');
|
|
443
|
-
const oauthToApiKeyMessages = _toAnthropicMessagesForTest(oauthHistory);
|
|
444
|
-
const apiKeyResult = oauthToApiKeyMessages
|
|
445
|
-
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
446
|
-
.find((block) => block.type === 'tool_result');
|
|
447
|
-
assert.deepEqual(apiKeyResult.content, [{ type: 'tool_reference', tool_name: 'mcp__demo__ping' }]);
|
|
448
|
-
const apiKeyTools = _anthropicApiKeyTest.deferredAnthropicTools(base, oauthHistory, { session });
|
|
449
|
-
assert.equal(apiKeyTools.find((tool) => tool.name === 'mcp__demo__ping')?.deferLoading, true);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
// Wraps an array of Anthropic SSE event objects in a minimal Response-like
|
|
453
|
-
// shape exposing the single `body.getReader()` API that parseSSEStream uses.
|
|
454
|
-
// Each event becomes a `data: <json>` SSE frame, preceded by its `event:` line.
|
|
455
|
-
function anthropicSseResponse(events) {
|
|
456
|
-
const encoder = new TextEncoder();
|
|
457
|
-
const frames = events.map((e) => {
|
|
458
|
-
const type = e.type || 'message';
|
|
459
|
-
return `event: ${type}\ndata: ${JSON.stringify(e)}\n\n`;
|
|
460
|
-
});
|
|
461
|
-
const chunks = frames.map((f) => encoder.encode(f));
|
|
462
|
-
let i = 0;
|
|
463
|
-
return {
|
|
464
|
-
body: {
|
|
465
|
-
getReader() {
|
|
466
|
-
return {
|
|
467
|
-
read() {
|
|
468
|
-
if (i < chunks.length) return Promise.resolve({ done: false, value: chunks[i++] });
|
|
469
|
-
return Promise.resolve({ done: true, value: undefined });
|
|
470
|
-
},
|
|
471
|
-
cancel() { return Promise.resolve(); },
|
|
472
|
-
releaseLock() {},
|
|
473
|
-
};
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
test('anthropic SSE exposes refusal stop details and category metadata', async () => {
|
|
480
|
-
const result = await anthropicParseSSEStream(
|
|
481
|
-
anthropicSseResponse([
|
|
482
|
-
{ type: 'message_start', message: { model: 'claude-fable-5', usage: { input_tokens: 1 } } },
|
|
483
|
-
{
|
|
484
|
-
type: 'message_delta',
|
|
485
|
-
delta: {
|
|
486
|
-
stop_reason: 'refusal',
|
|
487
|
-
stop_details: { classifier: 'safety' },
|
|
488
|
-
category: 'policy',
|
|
489
|
-
},
|
|
490
|
-
usage: { output_tokens: 0 },
|
|
491
|
-
},
|
|
492
|
-
{ type: 'message_stop' },
|
|
493
|
-
]),
|
|
494
|
-
null, () => {}, () => {}, () => {}, {}, null,
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
assert.equal(result.stopReason, 'refusal');
|
|
498
|
-
assert.deepEqual(result.stopDetails, { classifier: 'safety', category: 'policy' });
|
|
499
|
-
assert.equal(result.content, '');
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
function compatResponsesEventStream(events) {
|
|
503
|
-
return {
|
|
504
|
-
async *[Symbol.asyncIterator]() {
|
|
505
|
-
for (const event of events) yield event;
|
|
506
|
-
},
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Minimal 200-OK Response-like shape for the HTTP/SSE Responses path: frames
|
|
511
|
-
// each event as `event:<type>\ndata:<json>\n\n`, delivered synchronously so the
|
|
512
|
-
// semantic-idle watchdog never arms during the test.
|
|
513
|
-
function httpSseResponse(events) {
|
|
514
|
-
const encoder = new TextEncoder();
|
|
515
|
-
const chunks = events.map((e) => encoder.encode(`event: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`));
|
|
516
|
-
let i = 0;
|
|
517
|
-
return {
|
|
518
|
-
status: 200,
|
|
519
|
-
ok: true,
|
|
520
|
-
headers: new Map(),
|
|
521
|
-
body: {
|
|
522
|
-
getReader() {
|
|
523
|
-
return {
|
|
524
|
-
read() {
|
|
525
|
-
return i < chunks.length
|
|
526
|
-
? Promise.resolve({ done: false, value: chunks[i++] })
|
|
527
|
-
: Promise.resolve({ done: true, value: undefined });
|
|
528
|
-
},
|
|
529
|
-
cancel() { return Promise.resolve(); },
|
|
530
|
-
releaseLock() {},
|
|
531
|
-
};
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// === 1. openai-compat ======================================================
|
|
538
|
-
// Chat path: parseToolCalls(choice, label) openai-compat.mjs:957
|
|
539
|
-
// Responses path: parseResponsesToolCalls(response,...) openai-compat.mjs:972
|
|
540
|
-
// Both exported (added `export` keyword only).
|
|
541
|
-
|
|
542
|
-
test('openai-compat (chat): native tool_calls → canonical toolCalls', () => {
|
|
543
|
-
const choice = {
|
|
544
|
-
message: {
|
|
545
|
-
tool_calls: [{
|
|
546
|
-
id: 'call_1',
|
|
547
|
-
type: 'function',
|
|
548
|
-
function: { name: 'grep', arguments: '{"pattern":"x"}' },
|
|
549
|
-
}],
|
|
550
|
-
},
|
|
551
|
-
finish_reason: 'tool_calls',
|
|
552
|
-
};
|
|
553
|
-
const out = compatParseToolCalls(choice, 'test');
|
|
554
|
-
assert.deepEqual(out, [{ id: 'call_1', name: 'grep', arguments: { pattern: 'x' } }]);
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
test('openai-compat (chat): no tool_calls → undefined', () => {
|
|
558
|
-
assert.equal(compatParseToolCalls({ message: { content: 'hi' }, finish_reason: 'stop' }, 'test'), undefined);
|
|
559
|
-
assert.equal(compatParseToolCalls({ message: { tool_calls: [] } }, 'test'), undefined);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
test('openai-compat (responses): native function_call → canonical toolCalls', () => {
|
|
563
|
-
const response = {
|
|
564
|
-
status: 'completed',
|
|
565
|
-
output: [{
|
|
566
|
-
type: 'function_call',
|
|
567
|
-
call_id: 'fc_1',
|
|
568
|
-
name: 'read',
|
|
569
|
-
arguments: '{"path":"a"}',
|
|
570
|
-
}],
|
|
571
|
-
};
|
|
572
|
-
const out = compatParseResponsesToolCalls(response, 'test');
|
|
573
|
-
assert.deepEqual(out, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
test('openai-compat (responses): no function_call → undefined', () => {
|
|
577
|
-
assert.equal(compatParseResponsesToolCalls({ status: 'completed', output: [] }, 'test'), undefined);
|
|
578
|
-
assert.equal(compatParseResponsesToolCalls({ output: [{ type: 'message', content: [] }] }, 'test'), undefined);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// Native convergence: a completed function tool_call whose arguments JSON is
|
|
582
|
-
// malformed must NOT make the provider layer throw or swallow to {}. The parse
|
|
583
|
-
// failure rides through on the call's `arguments` slot as an invalid-args
|
|
584
|
-
// marker so the dispatch loop can return an is_error tool_result and let the
|
|
585
|
-
// model self-correct in the same turn.
|
|
586
|
-
test('openai-compat (chat): malformed tool_calls args → invalid-args marker (provider does not throw)', () => {
|
|
587
|
-
const choice = {
|
|
588
|
-
message: {
|
|
589
|
-
tool_calls: [{
|
|
590
|
-
id: 'call_bad',
|
|
591
|
-
type: 'function',
|
|
592
|
-
function: { name: 'grep', arguments: '{"pattern": dispatchAiWrapped}' },
|
|
593
|
-
}],
|
|
594
|
-
},
|
|
595
|
-
finish_reason: 'tool_calls',
|
|
596
|
-
};
|
|
597
|
-
const out = compatParseToolCalls(choice, 'test');
|
|
598
|
-
assert.equal(Array.isArray(out), true);
|
|
599
|
-
assert.equal(out.length, 1);
|
|
600
|
-
assert.equal(out[0].id, 'call_bad');
|
|
601
|
-
assert.equal(out[0].name, 'grep');
|
|
602
|
-
assert.equal(isInvalidToolArgsMarker(out[0].arguments), true);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test('openai-compat (responses): malformed function_call args → invalid-args marker (no throw)', () => {
|
|
606
|
-
const response = {
|
|
607
|
-
status: 'completed',
|
|
608
|
-
output: [{
|
|
609
|
-
type: 'function_call',
|
|
610
|
-
call_id: 'fc_bad',
|
|
611
|
-
name: 'read',
|
|
612
|
-
arguments: '{path:}',
|
|
613
|
-
}],
|
|
614
|
-
};
|
|
615
|
-
const out = compatParseResponsesToolCalls(response, 'test');
|
|
616
|
-
assert.equal(Array.isArray(out), true);
|
|
617
|
-
assert.equal(out.length, 1);
|
|
618
|
-
assert.equal(out[0].id, 'fc_bad');
|
|
619
|
-
assert.equal(out[0].name, 'read');
|
|
620
|
-
assert.equal(isInvalidToolArgsMarker(out[0].arguments), true);
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
test('openai-compat/xai Responses stream: response.completed salvages deferred function_call id/name', async () => {
|
|
624
|
-
const captured = [];
|
|
625
|
-
const out = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
626
|
-
{ type: 'response.created', response: { id: 'resp_1', model: 'grok' } },
|
|
627
|
-
{
|
|
628
|
-
type: 'response.function_call_arguments.done',
|
|
629
|
-
item_id: 'fc_item_1',
|
|
630
|
-
arguments: '{"path":"a"}',
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
type: 'response.completed',
|
|
634
|
-
response: {
|
|
635
|
-
id: 'resp_1',
|
|
636
|
-
model: 'grok',
|
|
637
|
-
status: 'completed',
|
|
638
|
-
output: [{
|
|
639
|
-
type: 'function_call',
|
|
640
|
-
id: 'fc_item_1',
|
|
641
|
-
call_id: 'fc_1',
|
|
642
|
-
name: 'read',
|
|
643
|
-
arguments: '{"path":"a"}',
|
|
644
|
-
}],
|
|
645
|
-
},
|
|
646
|
-
},
|
|
647
|
-
]), {
|
|
648
|
-
label: 'test',
|
|
649
|
-
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
650
|
-
responseOutputText: () => '',
|
|
651
|
-
onToolCall: (call) => captured.push(call),
|
|
652
|
-
});
|
|
653
|
-
assert.deepEqual(out.toolCalls, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
654
|
-
assert.deepEqual(captured, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// Missing-terminal + partial-tool gate (shared tool-stream-state.mjs tracker):
|
|
658
|
-
// a Responses stream that streams a CUSTOM tool call's input then truncates
|
|
659
|
-
// before response.output_item.done / response.completed. The call never lands
|
|
660
|
-
// in pendingCalls or toolCalls, so the truncation error must be gated as
|
|
661
|
-
// pendingToolUse via the shared active tool-item tracker (activeToolItems),
|
|
662
|
-
// NOT accepted as a text-only partial-final that would drop the in-flight tool.
|
|
663
|
-
test('openai-compat/xai Responses stream: mid custom-tool-input truncation is gated pendingToolUse via shared tracker', async () => {
|
|
664
|
-
const rejected = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
665
|
-
{ type: 'response.created', response: { id: 'resp_ct', model: 'grok' } },
|
|
666
|
-
{ type: 'response.output_text.delta', delta: 'partial ' },
|
|
667
|
-
{ type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_item_1' } },
|
|
668
|
-
{ type: 'response.custom_tool_call_input.delta', item_id: 'ct_item_1', delta: '{"x":' },
|
|
669
|
-
// stream truncates here: no output_item.done, no response.completed.
|
|
670
|
-
]), {
|
|
671
|
-
label: 'test',
|
|
672
|
-
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
673
|
-
responseOutputText: () => '',
|
|
674
|
-
onTextDelta: () => {},
|
|
675
|
-
}).then(() => null, (e) => e);
|
|
676
|
-
assert.ok(rejected, 'expected the truncated stream to reject');
|
|
677
|
-
assert.equal(rejected.streamStalled, true);
|
|
678
|
-
// The in-flight custom tool must gate partial-final success even though it
|
|
679
|
-
// never reached pendingCalls/toolCalls — the active tracker carries it.
|
|
680
|
-
assert.equal(rejected.pendingToolUse, true);
|
|
681
|
-
assert.equal(rejected.partialContent, 'partial ');
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
// Precision half of the same tracker: once the tool call fully COMPLETES
|
|
685
|
-
// (output_item.done clears the active item) and text keeps streaming, a later
|
|
686
|
-
// truncation is a plain text-only partial — the tracker having been cleared is
|
|
687
|
-
// what lets pendingToolUse fall back to the real emit/pending state.
|
|
688
|
-
test('openai-compat/xai Responses stream: completed tool then trailing-text truncation clears active tracker', async () => {
|
|
689
|
-
let emitted = 0;
|
|
690
|
-
const rejected = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
691
|
-
{ type: 'response.created', response: { id: 'resp_done', model: 'grok' } },
|
|
692
|
-
{ type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_done_1' } },
|
|
693
|
-
{ type: 'response.custom_tool_call_input.delta', item_id: 'ct_done_1', delta: '{"x":1}' },
|
|
694
|
-
{ type: 'response.output_item.done', item: { type: 'custom_tool_call', id: 'ct_done_1', name: 'load_tool', input: '{"x":1}' } },
|
|
695
|
-
{ type: 'response.output_text.delta', delta: 'after tool ' },
|
|
696
|
-
// truncates before response.completed.
|
|
697
|
-
]), {
|
|
698
|
-
label: 'test',
|
|
699
|
-
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
700
|
-
responseOutputText: () => '',
|
|
701
|
-
onTextDelta: () => {},
|
|
702
|
-
onToolCall: () => { emitted += 1; },
|
|
703
|
-
}).then(() => null, (e) => e);
|
|
704
|
-
assert.ok(rejected, 'expected the truncated stream to reject');
|
|
705
|
-
assert.equal(rejected.streamStalled, true);
|
|
706
|
-
// A tool WAS emitted this turn, so the turn is still unsafe/tool-bearing —
|
|
707
|
-
// pendingToolUse stays true off the emit state, not a stale active latch.
|
|
708
|
-
assert.ok(emitted >= 1);
|
|
709
|
-
assert.equal(rejected.pendingToolUse, true);
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// Reviewer fix: function output_item.done must delete the pendingCalls itemId
|
|
713
|
-
// before recomputing toolInFlight — otherwise a fully-completed function call
|
|
714
|
-
// keeps pendingCalls.size > 0 forever, and a later max-output cutoff on trailing
|
|
715
|
-
// text is misclassified as a truncated tool-in-flight stall instead of a clean
|
|
716
|
-
// length completion.
|
|
717
|
-
test('openai-compat/xai Responses stream: completed function call clears pendingCalls so max-output cutoff is clean length', async () => {
|
|
718
|
-
const out = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
719
|
-
{ type: 'response.created', response: { id: 'resp_len', model: 'grok' } },
|
|
720
|
-
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_len', name: 'read', call_id: 'fc_len' } },
|
|
721
|
-
{ type: 'response.function_call_arguments.done', item_id: 'fi_len', arguments: '{"path":"a"}' },
|
|
722
|
-
{ type: 'response.output_item.done', item: { type: 'function_call', id: 'fi_len', call_id: 'fc_len', name: 'read', arguments: '{"path":"a"}' } },
|
|
723
|
-
{ type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
|
|
724
|
-
]), {
|
|
725
|
-
label: 'test',
|
|
726
|
-
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
727
|
-
responseOutputText: () => '',
|
|
728
|
-
});
|
|
729
|
-
// The completed call drained pendingCalls + the active tracker, so the
|
|
730
|
-
// max-output cutoff is a clean length truncation (no in-flight-tool stall).
|
|
731
|
-
assert.equal(out.stopReason, 'length');
|
|
732
|
-
assert.deepEqual(out.toolCalls, [{ id: 'fc_len', name: 'read', arguments: { path: 'a' } }]);
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
// Reviewer fix (HTTP/SSE): a max_output_tokens cutoff while a function call is
|
|
736
|
-
// still in flight (added + partial args, no output_item.done) must NOT mark a
|
|
737
|
-
// clean length completion — the tool arguments were truncated. Mirror compat:
|
|
738
|
-
// throw a stream-stalled pendingToolUse error so the loop gates/retries.
|
|
739
|
-
test('openai-oauth HTTP/SSE Responses: max-output cutoff with function call in flight → stream-stalled pendingToolUse', async () => {
|
|
740
|
-
const rejected = await sendViaHttpSse({
|
|
741
|
-
auth: { type: 'openai-direct', apiKey: 'k' },
|
|
742
|
-
body: { model: 'gpt', tools: [] },
|
|
743
|
-
useModel: 'gpt',
|
|
744
|
-
fetchFn: async () => httpSseResponse([
|
|
745
|
-
{ type: 'response.created', response: { id: 'r', model: 'gpt' } },
|
|
746
|
-
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_1', name: 'read', call_id: 'fc_1' } },
|
|
747
|
-
{ type: 'response.function_call_arguments.delta', item_id: 'fi_1', delta: '{"path":' },
|
|
748
|
-
{ type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
|
|
749
|
-
]),
|
|
750
|
-
}).then(() => null, (e) => e);
|
|
751
|
-
assert.ok(rejected, 'expected the truncated-tool cutoff to reject');
|
|
752
|
-
assert.equal(rejected.streamStalled, true);
|
|
753
|
-
assert.equal(rejected.pendingToolUse, true);
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
// Reviewer fix (HTTP/SSE): function output_item.done must delete pendingCalls[id]
|
|
757
|
-
// before recomputing _toolInFlight — otherwise the completed call keeps
|
|
758
|
-
// pendingCalls.size > 0 and a later max-output cutoff is misread as a truncated
|
|
759
|
-
// tool. A fully-completed call before the cutoff must be a clean length result.
|
|
760
|
-
test('openai-oauth HTTP/SSE Responses: completed function call clears pendingCalls so max-output cutoff is clean length', async () => {
|
|
761
|
-
const out = await sendViaHttpSse({
|
|
762
|
-
auth: { type: 'openai-direct', apiKey: 'k' },
|
|
763
|
-
body: { model: 'gpt', tools: [] },
|
|
764
|
-
useModel: 'gpt',
|
|
765
|
-
fetchFn: async () => httpSseResponse([
|
|
766
|
-
{ type: 'response.created', response: { id: 'r', model: 'gpt' } },
|
|
767
|
-
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_1', name: 'read', call_id: 'fc_1' } },
|
|
768
|
-
{ type: 'response.function_call_arguments.done', item_id: 'fi_1', arguments: '{"path":"a"}' },
|
|
769
|
-
{ type: 'response.output_item.done', item: { type: 'function_call', id: 'fi_1', call_id: 'fc_1', name: 'read', arguments: '{"path":"a"}' } },
|
|
770
|
-
{ type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
|
|
771
|
-
]),
|
|
772
|
-
});
|
|
773
|
-
assert.equal(out.stopReason, 'length');
|
|
774
|
-
assert.deepEqual(out.toolCalls, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
test('openai-compat/xai Responses: freeform apply_patch downgrades to function schema', () => {
|
|
778
|
-
const tools = _toResponsesToolsForTest(PATCH_TOOL_DEFS);
|
|
779
|
-
const patch = tools.find((tool) => tool.name === 'apply_patch');
|
|
780
|
-
assert.equal(patch.type, 'function');
|
|
781
|
-
assert.equal(patch.format, undefined);
|
|
782
|
-
assert.equal(patch.parameters?.properties?.patch?.type, 'string');
|
|
783
|
-
assert.deepEqual(patch.parameters?.required, ['patch']);
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
test('openai-compat/xai Responses: load_tool downgrades from tool_search to function schema', () => {
|
|
787
|
-
const loadTool = {
|
|
788
|
-
name: 'load_tool',
|
|
789
|
-
description: 'load tools',
|
|
790
|
-
inputSchema: {
|
|
791
|
-
type: 'object',
|
|
792
|
-
properties: { names: { type: 'array', items: { type: 'string' } } },
|
|
793
|
-
},
|
|
794
|
-
};
|
|
795
|
-
const [xaiTool] = _toResponsesToolsForTest([loadTool], { provider: 'xai' });
|
|
796
|
-
assert.equal(xaiTool.type, 'function');
|
|
797
|
-
assert.equal(xaiTool.name, 'load_tool');
|
|
798
|
-
assert.equal(xaiTool.execution, undefined);
|
|
799
|
-
assert.equal(xaiTool.parameters, loadTool.inputSchema);
|
|
800
|
-
|
|
801
|
-
const [openaiTool] = _toResponsesToolsForTest([loadTool], { provider: 'openai' });
|
|
802
|
-
assert.equal(openaiTool.type, 'tool_search');
|
|
803
|
-
assert.equal(openaiTool.execution, 'client');
|
|
804
|
-
assert.equal(openaiTool.name, undefined);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
test('openai-compat/xai Responses: load_tool history replays as function_call', () => {
|
|
808
|
-
const { input } = _toXaiResponsesInputForTest([
|
|
809
|
-
{ role: 'user', content: 'load a tool' },
|
|
810
|
-
{
|
|
811
|
-
role: 'assistant',
|
|
812
|
-
content: '',
|
|
813
|
-
toolCalls: [{ id: 'call_load_1', name: 'load_tool', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
|
|
814
|
-
},
|
|
815
|
-
{
|
|
816
|
-
role: 'tool',
|
|
817
|
-
toolCallId: 'call_load_1',
|
|
818
|
-
content: '{}',
|
|
819
|
-
nativeToolSearch: { openaiTools: [{ name: 'read' }] },
|
|
820
|
-
},
|
|
821
|
-
], {
|
|
822
|
-
xaiResponses: {
|
|
823
|
-
previousResponseId: 'resp_same',
|
|
824
|
-
seenMessageCount: 0,
|
|
825
|
-
model: 'grok-4.5',
|
|
826
|
-
},
|
|
827
|
-
}, { model: 'grok-4.5' });
|
|
828
|
-
assert.equal(input.some((item) => item.type === 'tool_search_call'), false);
|
|
829
|
-
assert.equal(input.some((item) => item.type === 'tool_search_output'), false);
|
|
830
|
-
const call = input.find((item) => item.type === 'function_call' && item.name === 'load_tool');
|
|
831
|
-
assert.equal(call.call_id, 'call_load_1');
|
|
832
|
-
assert.deepEqual(JSON.parse(call.arguments), { names: ['read'] });
|
|
833
|
-
assert.equal(input.some((item) => item.type === 'function_call_output' && item.call_id === 'call_load_1'), true);
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
test('openai-compat/xai store=false continuation replays the full conversation with encrypted reasoning', () => {
|
|
837
|
-
const encrypted = {
|
|
838
|
-
type: 'reasoning',
|
|
839
|
-
id: 'reasoning_1',
|
|
840
|
-
encrypted_content: 'opaque-ciphertext',
|
|
841
|
-
summary: [],
|
|
842
|
-
};
|
|
843
|
-
const { input, previousResponseId, startIndex } = _toXaiResponsesInputForTest([
|
|
844
|
-
{ role: 'user', content: 'call read' },
|
|
845
|
-
{
|
|
846
|
-
role: 'assistant',
|
|
847
|
-
content: '',
|
|
848
|
-
toolCalls: [{ id: 'call_1', name: 'read', arguments: { path: 'a' } }],
|
|
849
|
-
},
|
|
850
|
-
{ role: 'tool', toolCallId: 'call_1', content: 'contents' },
|
|
851
|
-
], {
|
|
852
|
-
xaiResponses: {
|
|
853
|
-
previousResponseId: null,
|
|
854
|
-
responseId: 'resp_unstored',
|
|
855
|
-
store: false,
|
|
856
|
-
encryptedReasoningItems: [encrypted],
|
|
857
|
-
seenMessageCount: 1,
|
|
858
|
-
model: 'grok-4.5',
|
|
859
|
-
},
|
|
860
|
-
}, { model: 'grok-4.5' });
|
|
861
|
-
assert.equal(previousResponseId, null);
|
|
862
|
-
assert.equal(startIndex, 0);
|
|
863
|
-
assert.equal(input[0].role, 'user');
|
|
864
|
-
assert.deepEqual(input[1], encrypted);
|
|
865
|
-
assert.equal(input[2].call_id, 'call_1');
|
|
866
|
-
assert.equal(input[3].call_id, 'call_1');
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
test('openai-compat/xai store=false second tool round retains original system/user and both tool results', () => {
|
|
870
|
-
const firstReasoning = {
|
|
871
|
-
type: 'reasoning',
|
|
872
|
-
id: 'reasoning_1',
|
|
873
|
-
encrypted_content: 'opaque-first',
|
|
874
|
-
summary: [],
|
|
875
|
-
};
|
|
876
|
-
const secondReasoning = {
|
|
877
|
-
type: 'reasoning',
|
|
878
|
-
id: 'reasoning_2',
|
|
879
|
-
encrypted_content: 'opaque-second',
|
|
880
|
-
summary: [],
|
|
881
|
-
};
|
|
882
|
-
const { input, previousResponseId, startIndex } = _toXaiResponsesInputForTest([
|
|
883
|
-
{ role: 'system', content: 'original system' },
|
|
884
|
-
{ role: 'user', content: 'original user request' },
|
|
885
|
-
{ role: 'assistant', content: '', toolCalls: [{ id: 'call_1', name: 'read', arguments: { path: 'a' } }] },
|
|
886
|
-
{ role: 'tool', toolCallId: 'call_1', content: 'first tool result' },
|
|
887
|
-
{ role: 'assistant', content: '', toolCalls: [{ id: 'call_2', name: 'grep', arguments: { pattern: 'x' } }] },
|
|
888
|
-
{ role: 'tool', toolCallId: 'call_2', content: 'second tool result' },
|
|
889
|
-
], {
|
|
890
|
-
xaiResponses: {
|
|
891
|
-
previousResponseId: null,
|
|
892
|
-
responseId: 'resp_unstored_2',
|
|
893
|
-
store: false,
|
|
894
|
-
encryptedReasoningHistory: [
|
|
895
|
-
{ messageIndex: 2, items: [firstReasoning] },
|
|
896
|
-
{ messageIndex: 4, items: [secondReasoning] },
|
|
897
|
-
],
|
|
898
|
-
seenMessageCount: 4,
|
|
899
|
-
model: 'grok-4.5',
|
|
900
|
-
},
|
|
901
|
-
}, { model: 'grok-4.5' });
|
|
902
|
-
assert.equal(previousResponseId, null);
|
|
903
|
-
assert.equal(startIndex, 0);
|
|
904
|
-
assert.equal(input[0].role, 'system');
|
|
905
|
-
assert.equal(input[0].content[0].text, 'original system');
|
|
906
|
-
assert.equal(input[1].role, 'user');
|
|
907
|
-
assert.equal(input[1].content[0].text, 'original user request');
|
|
908
|
-
assert.deepEqual(input.filter((item) => item.type === 'reasoning'), [firstReasoning, secondReasoning]);
|
|
909
|
-
assert.deepEqual(
|
|
910
|
-
input.filter((item) => item.type === 'function_call').map((item) => item.call_id),
|
|
911
|
-
['call_1', 'call_2'],
|
|
912
|
-
);
|
|
913
|
-
assert.deepEqual(
|
|
914
|
-
input.filter((item) => item.type === 'function_call_output').map((item) => item.output),
|
|
915
|
-
['first tool result', 'second tool result'],
|
|
916
|
-
);
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
test('openai-compat/xai Responses: model switch drops prior tool transcript history', () => {
|
|
920
|
-
const { input, previousResponseId, continuationResetReason } = _toXaiResponsesInputForTest([
|
|
921
|
-
{ role: 'system', content: 'sys' },
|
|
922
|
-
{ role: 'user', content: 'before switch' },
|
|
923
|
-
{
|
|
924
|
-
role: 'assistant',
|
|
925
|
-
content: '',
|
|
926
|
-
toolCalls: [{ id: 'call_load_1', name: 'load_tool', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
|
|
927
|
-
},
|
|
928
|
-
{ role: 'tool', toolCallId: 'call_load_1', content: '{"loaded":["read"]}' },
|
|
929
|
-
{ role: 'user', content: 'after switch' },
|
|
930
|
-
], {
|
|
931
|
-
xaiResponses: {
|
|
932
|
-
previousResponseId: 'resp_old',
|
|
933
|
-
seenMessageCount: 4,
|
|
934
|
-
model: 'grok-4.20',
|
|
935
|
-
},
|
|
936
|
-
}, { model: 'grok-4.5' });
|
|
937
|
-
assert.equal(previousResponseId, null);
|
|
938
|
-
assert.equal(continuationResetReason, 'model_changed');
|
|
939
|
-
const serialized = JSON.stringify(input);
|
|
940
|
-
assert.equal(serialized.includes('tool_search'), false);
|
|
941
|
-
assert.equal(serialized.includes('function_call'), false);
|
|
942
|
-
assert.equal(serialized.includes('function_call_output'), false);
|
|
943
|
-
assert.deepEqual(input.map((item) => item.role), ['system', 'user', 'user']);
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
test('openai-compat/xai Responses: first Grok request after provider switch drops prior tool transcript history', () => {
|
|
947
|
-
const { input, previousResponseId, continuationResetReason } = _toXaiResponsesInputForTest([
|
|
948
|
-
{ role: 'system', content: 'sys' },
|
|
949
|
-
{ role: 'user', content: 'before switch' },
|
|
950
|
-
{
|
|
951
|
-
role: 'assistant',
|
|
952
|
-
content: '',
|
|
953
|
-
toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: '*** Begin Patch\n*** End Patch\n' } }],
|
|
954
|
-
},
|
|
955
|
-
{ role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
|
|
956
|
-
{ role: 'user', content: 'after switch' },
|
|
957
|
-
], {}, { model: 'grok-4.5' });
|
|
958
|
-
assert.equal(previousResponseId, null);
|
|
959
|
-
assert.equal(continuationResetReason, null);
|
|
960
|
-
const serialized = JSON.stringify(input);
|
|
961
|
-
assert.equal(serialized.includes('function_call'), false);
|
|
962
|
-
assert.equal(serialized.includes('function_call_output'), false);
|
|
963
|
-
assert.deepEqual(input.map((item) => item.role), ['system', 'user', 'user']);
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
test('openai-oauth Responses: load_tool uses native tool_search history', () => {
|
|
967
|
-
const loadTool = {
|
|
968
|
-
name: 'load_tool',
|
|
969
|
-
description: 'load tools',
|
|
970
|
-
inputSchema: {
|
|
971
|
-
type: 'object',
|
|
972
|
-
properties: { names: { type: 'array', items: { type: 'string' } } },
|
|
973
|
-
},
|
|
974
|
-
};
|
|
975
|
-
const body = buildOpenAIOAuthRequestBody([
|
|
976
|
-
{ role: 'system', content: 'sys' },
|
|
977
|
-
{ role: 'user', content: 'load a tool' },
|
|
978
|
-
{
|
|
979
|
-
role: 'assistant',
|
|
980
|
-
content: '',
|
|
981
|
-
toolCalls: [{ id: 'call_load_1', name: 'tool_search', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
|
|
982
|
-
},
|
|
983
|
-
{
|
|
984
|
-
role: 'tool',
|
|
985
|
-
toolCallId: 'call_load_1',
|
|
986
|
-
content: 'Loaded deferred tools: read',
|
|
987
|
-
nativeToolSearch: {
|
|
988
|
-
provider: 'openai-oauth',
|
|
989
|
-
toolReferences: ['read'],
|
|
990
|
-
openaiTools: [{
|
|
991
|
-
type: 'function',
|
|
992
|
-
name: 'read',
|
|
993
|
-
description: 'read',
|
|
994
|
-
defer_loading: true,
|
|
995
|
-
parameters: { type: 'object', properties: {} },
|
|
996
|
-
}],
|
|
997
|
-
},
|
|
998
|
-
},
|
|
999
|
-
], 'gpt-5.5', [loadTool], {});
|
|
1000
|
-
assert.equal(JSON.stringify(body).includes('tool_search'), true);
|
|
1001
|
-
assert.deepEqual(body.tools, [{
|
|
1002
|
-
type: 'tool_search',
|
|
1003
|
-
execution: 'client',
|
|
1004
|
-
description: loadTool.description,
|
|
1005
|
-
parameters: loadTool.inputSchema,
|
|
1006
|
-
}]);
|
|
1007
|
-
const call = body.input.find((item) => item.type === 'tool_search_call');
|
|
1008
|
-
assert.deepEqual(call, {
|
|
1009
|
-
type: 'tool_search_call',
|
|
1010
|
-
call_id: 'call_load_1',
|
|
1011
|
-
execution: 'client',
|
|
1012
|
-
arguments: { names: ['read'] },
|
|
1013
|
-
});
|
|
1014
|
-
const output = body.input.find((item) => item.type === 'tool_search_output' && item.call_id === 'call_load_1');
|
|
1015
|
-
assert.deepEqual(output, {
|
|
1016
|
-
type: 'tool_search_output',
|
|
1017
|
-
call_id: 'call_load_1',
|
|
1018
|
-
status: 'completed',
|
|
1019
|
-
execution: 'client',
|
|
1020
|
-
tools: [{
|
|
1021
|
-
type: 'function',
|
|
1022
|
-
name: 'read',
|
|
1023
|
-
description: 'read',
|
|
1024
|
-
defer_loading: true,
|
|
1025
|
-
parameters: { type: 'object', properties: {} },
|
|
1026
|
-
}],
|
|
1027
|
-
});
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
test('openai-compat/xai Responses: custom_tool_call history replays as function_call', () => {
|
|
1031
|
-
const rawPatch = '*** Begin Patch\n*** Add File: xai-history.txt\n+ok\n*** End Patch\n';
|
|
1032
|
-
const { input } = _toXaiResponsesInputForTest([
|
|
1033
|
-
{ role: 'user', content: 'patch please' },
|
|
1034
|
-
{
|
|
1035
|
-
role: 'assistant',
|
|
1036
|
-
content: '',
|
|
1037
|
-
toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: rawPatch }, nativeType: 'custom_tool_call' }],
|
|
1038
|
-
},
|
|
1039
|
-
{ role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
|
|
1040
|
-
], {
|
|
1041
|
-
xaiResponses: {
|
|
1042
|
-
previousResponseId: 'resp_same',
|
|
1043
|
-
seenMessageCount: 0,
|
|
1044
|
-
model: 'grok-composer-2.5-fast',
|
|
1045
|
-
},
|
|
1046
|
-
}, { model: 'grok-composer-2.5-fast' });
|
|
1047
|
-
assert.equal(input.some((item) => item.type === 'custom_tool_call'), false);
|
|
1048
|
-
assert.equal(input.some((item) => item.type === 'custom_tool_call_output'), false);
|
|
1049
|
-
const call = input.find((item) => item.type === 'function_call' && item.name === 'apply_patch');
|
|
1050
|
-
assert.equal(call.call_id, 'call_patch_1');
|
|
1051
|
-
assert.deepEqual(JSON.parse(call.arguments), { patch: rawPatch });
|
|
1052
|
-
const output = input.find((item) => item.type === 'function_call_output');
|
|
1053
|
-
assert.equal(output.call_id, 'call_patch_1');
|
|
1054
|
-
assert.equal(output.output, 'OK');
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
// === 2. gemini =============================================================
|
|
1058
|
-
// parseToolCalls(parts) gemini.mjs:946 (exported — `export` keyword only).
|
|
1059
|
-
// id is a content hash → assert the `gemini_` prefix, not the exact value.
|
|
1060
|
-
|
|
1061
|
-
test('gemini: native functionCall parts → canonical toolCalls (hashed id)', () => {
|
|
1062
|
-
const parts = [{ functionCall: { name: 'read', args: { path: 'a' } } }];
|
|
1063
|
-
const out = geminiParseToolCalls(parts);
|
|
1064
|
-
assert.equal(Array.isArray(out), true);
|
|
1065
|
-
assert.equal(out.length, 1);
|
|
1066
|
-
assert.equal(out[0].name, 'read');
|
|
1067
|
-
assert.deepEqual(out[0].arguments, { path: 'a' });
|
|
1068
|
-
assert.match(out[0].id, /^gemini_/);
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
test('gemini: no functionCall parts → undefined', () => {
|
|
1072
|
-
assert.equal(geminiParseToolCalls([{ text: 'hello' }]), undefined);
|
|
1073
|
-
assert.equal(geminiParseToolCalls([]), undefined);
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
test('gemini leak guard: leaked <invoke> in part.text for known tool → recovered, no leak', () => {
|
|
1077
|
-
const leaked = 'Sure.\n<function_calls>\n<invoke name="read">\n<parameter name="path">a.txt</parameter>\n</invoke>\n</function_calls>';
|
|
1078
|
-
const texts = [];
|
|
1079
|
-
const captured = [];
|
|
1080
|
-
const guard = createGeminiTextLeakGuard({
|
|
1081
|
-
knownToolNames: LEAK_TOOLS,
|
|
1082
|
-
onTextDelta: (t) => texts.push(t),
|
|
1083
|
-
onToolCall: (c) => captured.push(c),
|
|
1084
|
-
});
|
|
1085
|
-
guard.feedText(leaked);
|
|
1086
|
-
guard.finalize();
|
|
1087
|
-
const content = guard.scrubAssistantText(leaked);
|
|
1088
|
-
const emitted = texts.join('');
|
|
1089
|
-
assert.equal(/<invoke|<function_calls|<parameter/.test(emitted), false);
|
|
1090
|
-
assert.equal(/<invoke|<function_calls|<parameter/.test(content), false);
|
|
1091
|
-
assert.ok(emitted.includes('Sure.'));
|
|
1092
|
-
assert.ok(content.includes('Sure.'));
|
|
1093
|
-
const leakedCalls = guard.getLeakedToolCalls();
|
|
1094
|
-
assert.equal(leakedCalls.length, 1);
|
|
1095
|
-
assert.equal(leakedCalls[0].name, 'read');
|
|
1096
|
-
assert.deepEqual(leakedCalls[0].arguments, { path: 'a.txt' });
|
|
1097
|
-
assert.match(leakedCalls[0].id, /^gemini_leaked_/);
|
|
1098
|
-
assert.equal(captured.length, 1);
|
|
1099
|
-
assert.equal(captured[0].name, 'read');
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
test('gemini leak guard: unknown tool name → text flushed, no synthetic call', () => {
|
|
1103
|
-
const leaked = '<function_calls>\n<invoke name="nonexistent_tool">\n<parameter name="x">1</parameter>\n</invoke>\n</function_calls>';
|
|
1104
|
-
const captured = [];
|
|
1105
|
-
const guard = createGeminiTextLeakGuard({
|
|
1106
|
-
knownToolNames: LEAK_TOOLS,
|
|
1107
|
-
onToolCall: (c) => captured.push(c),
|
|
1108
|
-
});
|
|
1109
|
-
guard.feedText(leaked);
|
|
1110
|
-
guard.finalize();
|
|
1111
|
-
const content = guard.scrubAssistantText(leaked);
|
|
1112
|
-
assert.equal(guard.getLeakedToolCalls().length, 0);
|
|
1113
|
-
assert.equal(captured.length, 0);
|
|
1114
|
-
assert.ok(content.includes('nonexistent_tool'));
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
test('gemini cache usage: official cached token fields are subsets of prompt tokens', () => {
|
|
1118
|
-
const direct = _resolveGeminiCacheUsage({
|
|
1119
|
-
usageMetadata: { promptTokenCount: 1000, cachedContentTokenCount: 400 },
|
|
1120
|
-
});
|
|
1121
|
-
assert.deepEqual({
|
|
1122
|
-
inputTokens: direct.inputTokens,
|
|
1123
|
-
reportedCachedTokens: direct.reportedCachedTokens,
|
|
1124
|
-
cachedTokens: direct.cachedTokens,
|
|
1125
|
-
cacheTokenSource: direct.cacheTokenSource,
|
|
1126
|
-
}, {
|
|
1127
|
-
inputTokens: 1000,
|
|
1128
|
-
reportedCachedTokens: 400,
|
|
1129
|
-
cachedTokens: 400,
|
|
1130
|
-
cacheTokenSource: 'usage_metadata',
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
const sdkAlias = _resolveGeminiCacheUsage({
|
|
1134
|
-
usageMetadata: { prompt_token_count: 1200, total_cached_tokens: 500 },
|
|
1135
|
-
});
|
|
1136
|
-
assert.equal(sdkAlias.inputTokens, 1200);
|
|
1137
|
-
assert.equal(sdkAlias.reportedCachedTokens, 500);
|
|
1138
|
-
assert.equal(sdkAlias.cachedTokens, 500);
|
|
1139
|
-
assert.notEqual(sdkAlias.inputTokens, 0, 'snake_case SDK aliases must remain visible to provider return usage');
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
test('gemini cache usage: total fallback excludes candidate and thought output tokens', () => {
|
|
1143
|
-
const camelCase = _resolveGeminiCacheUsage({
|
|
1144
|
-
usageMetadata: {
|
|
1145
|
-
totalTokenCount: 1000,
|
|
1146
|
-
candidatesTokenCount: 150,
|
|
1147
|
-
thoughtsTokenCount: 50,
|
|
1148
|
-
},
|
|
1149
|
-
});
|
|
1150
|
-
assert.equal(camelCase.inputTokens, 800);
|
|
1151
|
-
|
|
1152
|
-
const snakeCase = _resolveGeminiCacheUsage({
|
|
1153
|
-
usageMetadata: {
|
|
1154
|
-
total_token_count: 1200,
|
|
1155
|
-
candidates_token_count: 175,
|
|
1156
|
-
thoughts_token_count: 25,
|
|
1157
|
-
},
|
|
1158
|
-
});
|
|
1159
|
-
assert.equal(snakeCase.inputTokens, 1000);
|
|
1160
|
-
|
|
1161
|
-
const promptWins = _resolveGeminiCacheUsage({
|
|
1162
|
-
usageMetadata: {
|
|
1163
|
-
promptTokenCount: 900,
|
|
1164
|
-
totalTokenCount: 1200,
|
|
1165
|
-
candidatesTokenCount: 250,
|
|
1166
|
-
thoughtsTokenCount: 50,
|
|
1167
|
-
},
|
|
1168
|
-
});
|
|
1169
|
-
assert.equal(promptWins.inputTokens, 900);
|
|
1170
|
-
|
|
1171
|
-
const explicitZero = _resolveGeminiCacheUsage({
|
|
1172
|
-
usageMetadata: {
|
|
1173
|
-
prompt_token_count: 0,
|
|
1174
|
-
total_token_count: 1200,
|
|
1175
|
-
candidates_token_count: 175,
|
|
1176
|
-
thoughts_token_count: 25,
|
|
1177
|
-
},
|
|
1178
|
-
});
|
|
1179
|
-
assert.equal(explicitZero.inputTokens, 0);
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
test('gemini cache usage: clamps over-reported cache and falls back only for attached cachedContent', () => {
|
|
1183
|
-
const clamped = _resolveGeminiCacheUsage({
|
|
1184
|
-
usageMetadata: { promptTokenCount: 100, cachedContentTokenCount: 150 },
|
|
1185
|
-
});
|
|
1186
|
-
assert.equal(clamped.cachedTokens, 100);
|
|
1187
|
-
|
|
1188
|
-
const fallback = _resolveGeminiCacheUsage({
|
|
1189
|
-
usageMetadata: { promptTokenCount: 1000 },
|
|
1190
|
-
cachedContent: 'cachedContents/abc',
|
|
1191
|
-
providerState: { gemini: { cacheTokenSize: 250 } },
|
|
1192
|
-
});
|
|
1193
|
-
assert.equal(fallback.cachedTokens, 250);
|
|
1194
|
-
assert.equal(fallback.cacheTokenSource, 'cache_create_fallback');
|
|
1195
|
-
|
|
1196
|
-
const noFallbackWithoutAttachment = _resolveGeminiCacheUsage({
|
|
1197
|
-
usageMetadata: { promptTokenCount: 1000 },
|
|
1198
|
-
providerState: { gemini: { cacheTokenSize: 250 } },
|
|
1199
|
-
});
|
|
1200
|
-
assert.equal(noFallbackWithoutAttachment.cachedTokens, 0);
|
|
1201
|
-
assert.equal(noFallbackWithoutAttachment.cacheTokenSource, 'none');
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
// === 3. anthropic / anthropic-oauth ========================================
|
|
1205
|
-
// tool_use block parser lives in anthropic-oauth.mjs:936 parseSSEStream
|
|
1206
|
-
// (content_block_start/delta/stop → toolCalls.push). anthropic.mjs has NO
|
|
1207
|
-
// independent tool_use parser: it imports and reuses the SAME parseSSEStream
|
|
1208
|
-
// from anthropic-oauth.mjs (anthropic.mjs:12). So a single test covers both
|
|
1209
|
-
// providers — shared parser, no duplicate test needed.
|
|
1210
|
-
|
|
1211
|
-
test('anthropic(-oauth): streamed tool_use block → canonical toolCalls', async () => {
|
|
1212
|
-
const events = [
|
|
1213
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1214
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_1', name: 'shell' } },
|
|
1215
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"command":"ls"}' } },
|
|
1216
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1217
|
-
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
1218
|
-
{ type: 'message_stop' },
|
|
1219
|
-
];
|
|
1220
|
-
const captured = [];
|
|
1221
|
-
const result = await anthropicParseSSEStream(
|
|
1222
|
-
anthropicSseResponse(events),
|
|
1223
|
-
null, // signal
|
|
1224
|
-
() => {}, // abortStream
|
|
1225
|
-
() => {}, // onStreamDelta
|
|
1226
|
-
(call) => captured.push(call), // onToolCall
|
|
1227
|
-
{}, // state
|
|
1228
|
-
null, // onTextDelta
|
|
1229
|
-
);
|
|
1230
|
-
assert.deepEqual(result.toolCalls, [{ id: 'toolu_1', name: 'shell', arguments: { command: 'ls' } }]);
|
|
1231
|
-
// Eager dispatch fired the same call exactly once.
|
|
1232
|
-
assert.deepEqual(captured, [{ id: 'toolu_1', name: 'shell', arguments: { command: 'ls' } }]);
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
test('anthropic(-oauth): malformed streamed tool_use args → invalid-args marker, not {} dispatch', async () => {
|
|
1236
|
-
const events = [
|
|
1237
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1238
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_bad', name: 'shell' } },
|
|
1239
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"command": dispatchAiWrapped}' } },
|
|
1240
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1241
|
-
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
1242
|
-
{ type: 'message_stop' },
|
|
1243
|
-
];
|
|
1244
|
-
const captured = [];
|
|
1245
|
-
const result = await anthropicParseSSEStream(
|
|
1246
|
-
anthropicSseResponse(events),
|
|
1247
|
-
null,
|
|
1248
|
-
() => {},
|
|
1249
|
-
() => {},
|
|
1250
|
-
(call) => captured.push(call),
|
|
1251
|
-
{},
|
|
1252
|
-
null,
|
|
1253
|
-
);
|
|
1254
|
-
assert.equal(isInvalidToolArgsMarker(result.toolCalls[0].arguments), true);
|
|
1255
|
-
assert.equal(result.toolCalls[0].arguments.__rawArguments, '{"command": dispatchAiWrapped}');
|
|
1256
|
-
assert.equal(isInvalidToolArgsMarker(captured[0].arguments), true);
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
test('anthropic(-oauth): text-only stream → no toolCalls', async () => {
|
|
1260
|
-
const events = [
|
|
1261
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1262
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'text' } },
|
|
1263
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hello' } },
|
|
1264
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1265
|
-
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
1266
|
-
{ type: 'message_stop' },
|
|
1267
|
-
];
|
|
1268
|
-
const result = await anthropicParseSSEStream(
|
|
1269
|
-
anthropicSseResponse(events),
|
|
1270
|
-
null, () => {}, () => {}, () => {}, {}, null,
|
|
1271
|
-
);
|
|
1272
|
-
assert.equal(result.toolCalls, undefined);
|
|
1273
|
-
assert.equal(result.content, 'hello');
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
test('anthropic(-oauth): thinking + signature deltas → ordered thinkingBlocks before tool_use', async () => {
|
|
1277
|
-
const events = [
|
|
1278
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1279
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '' } },
|
|
1280
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'step ' } },
|
|
1281
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'one' } },
|
|
1282
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sig123' } },
|
|
1283
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1284
|
-
{ type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_9', name: 'shell' } },
|
|
1285
|
-
{ type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"command":"ls"}' } },
|
|
1286
|
-
{ type: 'content_block_stop', index: 1 },
|
|
1287
|
-
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
1288
|
-
{ type: 'message_stop' },
|
|
1289
|
-
];
|
|
1290
|
-
const result = await anthropicParseSSEStream(
|
|
1291
|
-
anthropicSseResponse(events),
|
|
1292
|
-
null, () => {}, () => {}, () => {}, {}, null,
|
|
1293
|
-
);
|
|
1294
|
-
assert.equal(result.hasThinkingContent, true);
|
|
1295
|
-
assert.deepEqual(result.thinkingBlocks, [
|
|
1296
|
-
{ type: 'thinking', thinking: 'step one', signature: 'sig123' },
|
|
1297
|
-
]);
|
|
1298
|
-
assert.deepEqual(result.toolCalls, [{ id: 'toolu_9', name: 'shell', arguments: { command: 'ls' } }]);
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
test('anthropic(-oauth): signature-only block (display omitted) → empty thinking kept with signature', async () => {
|
|
1302
|
-
const events = [
|
|
1303
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1304
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking' } },
|
|
1305
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sigABC' } },
|
|
1306
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1307
|
-
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
1308
|
-
{ type: 'message_stop' },
|
|
1309
|
-
];
|
|
1310
|
-
const result = await anthropicParseSSEStream(
|
|
1311
|
-
anthropicSseResponse(events),
|
|
1312
|
-
null, () => {}, () => {}, () => {}, {}, null,
|
|
1313
|
-
);
|
|
1314
|
-
assert.deepEqual(result.thinkingBlocks, [
|
|
1315
|
-
{ type: 'thinking', thinking: '', signature: 'sigABC' },
|
|
1316
|
-
]);
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
test('anthropic(-oauth): redacted_thinking round-trips exactly as {type,data} (no extra fields)', async () => {
|
|
1320
|
-
const events = [
|
|
1321
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1322
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'redacted_thinking', data: 'ENCRYPTED_PAYLOAD' } },
|
|
1323
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1324
|
-
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
1325
|
-
{ type: 'message_stop' },
|
|
1326
|
-
];
|
|
1327
|
-
const result = await anthropicParseSSEStream(
|
|
1328
|
-
anthropicSseResponse(events),
|
|
1329
|
-
null, () => {}, () => {}, () => {}, {}, null,
|
|
1330
|
-
);
|
|
1331
|
-
assert.deepEqual(result.thinkingBlocks, [
|
|
1332
|
-
{ type: 'redacted_thinking', data: 'ENCRYPTED_PAYLOAD' },
|
|
1333
|
-
]);
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
test('anthropic API-key and OAuth lower plain signed thinkingBlocks before text without tool calls', () => {
|
|
1337
|
-
const thinkingBlocks = [
|
|
1338
|
-
{ type: 'thinking', thinking: 'resume state', signature: 'sig-recovery-1' },
|
|
1339
|
-
{ type: 'redacted_thinking', data: 'ENCRYPTED_RECOVERY_STATE' },
|
|
1340
|
-
];
|
|
1341
|
-
const history = [
|
|
1342
|
-
{ role: 'user', content: 'write the answer' },
|
|
1343
|
-
{ role: 'assistant', content: 'partial answer', thinkingBlocks },
|
|
1344
|
-
{ role: 'user', content: 'resume directly' },
|
|
1345
|
-
];
|
|
1346
|
-
const expectedAssistantContent = [
|
|
1347
|
-
...thinkingBlocks,
|
|
1348
|
-
{ type: 'text', text: 'partial answer' },
|
|
1349
|
-
];
|
|
1350
|
-
|
|
1351
|
-
const apiKeyMessages = _toAnthropicMessagesForTest(history);
|
|
1352
|
-
const oauthMessages = _buildRequestBodyForCacheSmoke(
|
|
1353
|
-
history,
|
|
1354
|
-
'claude-sonnet-4-6',
|
|
1355
|
-
[],
|
|
1356
|
-
{},
|
|
1357
|
-
).messages;
|
|
1358
|
-
|
|
1359
|
-
for (const lowered of [apiKeyMessages, oauthMessages]) {
|
|
1360
|
-
const assistant = lowered.find((message) => message.role === 'assistant');
|
|
1361
|
-
assert.ok(assistant, 'plain recovery assistant turn must survive lowering');
|
|
1362
|
-
assert.deepEqual(assistant.content, expectedAssistantContent);
|
|
1363
|
-
assert.equal(assistant.content.some((block) => block.type === 'tool_use'), false);
|
|
1364
|
-
}
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
test('anthropic API-key and OAuth lower interrupted tool results with is_error', () => {
|
|
1368
|
-
const history = [
|
|
1369
|
-
{ role: 'user', content: 'run a tool' },
|
|
1370
|
-
{
|
|
1371
|
-
role: 'assistant',
|
|
1372
|
-
content: '',
|
|
1373
|
-
toolCalls: [{ id: 'toolu_interrupted_1', name: 'read', arguments: { path: 'x' } }],
|
|
1374
|
-
},
|
|
1375
|
-
{
|
|
1376
|
-
role: 'tool',
|
|
1377
|
-
content: 'Interrupted by user',
|
|
1378
|
-
toolCallId: 'toolu_interrupted_1',
|
|
1379
|
-
toolKind: 'error',
|
|
1380
|
-
},
|
|
1381
|
-
{ role: 'user', content: '[Request interrupted by user]' },
|
|
1382
|
-
];
|
|
1383
|
-
const apiKeyMessages = _toAnthropicMessagesForTest(history);
|
|
1384
|
-
const oauthMessages = _buildRequestBodyForCacheSmoke(
|
|
1385
|
-
history,
|
|
1386
|
-
'claude-sonnet-4-6',
|
|
1387
|
-
[],
|
|
1388
|
-
{},
|
|
1389
|
-
).messages;
|
|
1390
|
-
|
|
1391
|
-
for (const lowered of [apiKeyMessages, oauthMessages]) {
|
|
1392
|
-
const toolResult = lowered
|
|
1393
|
-
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
1394
|
-
.find((block) => (
|
|
1395
|
-
block?.type === 'tool_result'
|
|
1396
|
-
&& block.tool_use_id === 'toolu_interrupted_1'
|
|
1397
|
-
));
|
|
1398
|
-
assert.ok(toolResult);
|
|
1399
|
-
assert.equal(toolResult.is_error, true);
|
|
1400
|
-
assert.equal(JSON.stringify(lowered).includes('[Request interrupted by user]'), true);
|
|
1401
|
-
}
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
test('anthropic effort: legacy claude-3-7-sonnet gets NO adaptive thinking / effort beta', () => {
|
|
1405
|
-
const model = 'claude-3-7-sonnet-20250219';
|
|
1406
|
-
assert.equal(modelSupportsEffort(model), false);
|
|
1407
|
-
const body = _buildRequestBodyForCacheSmoke(
|
|
1408
|
-
[{ role: 'user', content: 'hi' }],
|
|
1409
|
-
model,
|
|
1410
|
-
[],
|
|
1411
|
-
{ effort: 'high' },
|
|
1412
|
-
);
|
|
1413
|
-
assert.equal(body.output_config, undefined);
|
|
1414
|
-
// Legacy path uses the budget_tokens shape, never thinking:adaptive.
|
|
1415
|
-
assert.notEqual(body.thinking?.type, 'adaptive');
|
|
1416
|
-
assert.equal(shouldIncludeEffortBeta(model, { effort: 'high' }), false);
|
|
1417
|
-
});
|
|
1418
|
-
|
|
1419
|
-
// --- Leaked tool-call recovery (shared parseSSEStream guard) ----------------
|
|
1420
|
-
// The model sometimes emits a tool call as plain text tags inside text_delta
|
|
1421
|
-
// instead of a native tool_use block. The guard (8th arg = known tool names)
|
|
1422
|
-
// suppresses the tags from the visible stream, removes them from content, and
|
|
1423
|
-
// synthesizes/dispatches a real tool call. anthropic.mjs reuses this SAME
|
|
1424
|
-
// parseSSEStream, so both providers are covered by one guard.
|
|
1425
|
-
const LEAK_TOOLS = new Set(['shell', 'read']);
|
|
1426
|
-
|
|
1427
|
-
function textDeltaEvents(chunks, stopReason = 'end_turn') {
|
|
1428
|
-
return [
|
|
1429
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1430
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'text' } },
|
|
1431
|
-
...chunks.map((text) => ({ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } })),
|
|
1432
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1433
|
-
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
|
|
1434
|
-
{ type: 'message_stop' },
|
|
1435
|
-
];
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
test('anthropic(-oauth) leak guard: leaked <function_calls>/<invoke> for known tool → recovered, no leak', async () => {
|
|
1439
|
-
const leaked = 'Sure.\n<function_calls>\n<invoke name="shell">\n<parameter name="command">ls -la</parameter>\n</invoke>\n</function_calls>';
|
|
1440
|
-
const texts = [];
|
|
1441
|
-
const captured = [];
|
|
1442
|
-
const result = await anthropicParseSSEStream(
|
|
1443
|
-
anthropicSseResponse(textDeltaEvents([leaked])),
|
|
1444
|
-
null, () => {}, () => {},
|
|
1445
|
-
(call) => captured.push(call),
|
|
1446
|
-
{},
|
|
1447
|
-
(t) => texts.push(t),
|
|
1448
|
-
LEAK_TOOLS,
|
|
1449
|
-
);
|
|
1450
|
-
const emitted = texts.join('');
|
|
1451
|
-
// Tags never reached the visible stream nor the returned content.
|
|
1452
|
-
assert.equal(/<invoke|<function_calls|<parameter/.test(emitted), false);
|
|
1453
|
-
assert.equal(/<invoke|<function_calls|<parameter/.test(result.content), false);
|
|
1454
|
-
assert.ok(emitted.includes('Sure.'));
|
|
1455
|
-
assert.ok(result.content.includes('Sure.'));
|
|
1456
|
-
// A real, dispatched tool call was synthesized.
|
|
1457
|
-
assert.equal(result.toolCalls.length, 1);
|
|
1458
|
-
assert.equal(result.toolCalls[0].name, 'shell');
|
|
1459
|
-
assert.deepEqual(result.toolCalls[0].arguments, { command: 'ls -la' });
|
|
1460
|
-
assert.equal(captured.length, 1);
|
|
1461
|
-
assert.equal(captured[0].name, 'shell');
|
|
1462
|
-
assert.deepEqual(captured[0].arguments, { command: 'ls -la' });
|
|
1463
|
-
});
|
|
1464
|
-
|
|
1465
|
-
test('anthropic(-oauth) leak guard: tags split across two text_delta chunks still detected', async () => {
|
|
1466
|
-
const a = '<invoke name="sh';
|
|
1467
|
-
const b = 'ell">\n<parameter name="command">pwd</parameter>\n</invoke>';
|
|
1468
|
-
const texts = [];
|
|
1469
|
-
const captured = [];
|
|
1470
|
-
const result = await anthropicParseSSEStream(
|
|
1471
|
-
anthropicSseResponse(textDeltaEvents([a, b])),
|
|
1472
|
-
null, () => {}, () => {},
|
|
1473
|
-
(call) => captured.push(call),
|
|
1474
|
-
{},
|
|
1475
|
-
(t) => texts.push(t),
|
|
1476
|
-
LEAK_TOOLS,
|
|
1477
|
-
);
|
|
1478
|
-
assert.equal(/<invoke|<parameter/.test(texts.join('')), false);
|
|
1479
|
-
assert.equal(result.toolCalls.length, 1);
|
|
1480
|
-
assert.equal(result.toolCalls[0].name, 'shell');
|
|
1481
|
-
assert.deepEqual(result.toolCalls[0].arguments, { command: 'pwd' });
|
|
1482
|
-
assert.deepEqual(captured[0].arguments, { command: 'pwd' });
|
|
1483
|
-
});
|
|
1484
|
-
|
|
1485
|
-
test('anthropic(-oauth) leak guard: unknown tool name → text flushed, no synthetic call', async () => {
|
|
1486
|
-
const leaked = '<function_calls>\n<invoke name="nonexistent_tool">\n<parameter name="x">1</parameter>\n</invoke>\n</function_calls>';
|
|
1487
|
-
const texts = [];
|
|
1488
|
-
const captured = [];
|
|
1489
|
-
const result = await anthropicParseSSEStream(
|
|
1490
|
-
anthropicSseResponse(textDeltaEvents([leaked])),
|
|
1491
|
-
null, () => {}, () => {},
|
|
1492
|
-
(call) => captured.push(call),
|
|
1493
|
-
{},
|
|
1494
|
-
(t) => texts.push(t),
|
|
1495
|
-
LEAK_TOOLS,
|
|
1496
|
-
);
|
|
1497
|
-
// Not a known tool: nothing recovered, and the text is preserved (never lost).
|
|
1498
|
-
assert.equal(result.toolCalls, undefined);
|
|
1499
|
-
assert.equal(captured.length, 0);
|
|
1500
|
-
assert.ok(texts.join('').includes('nonexistent_tool'));
|
|
1501
|
-
assert.ok(result.content.includes('nonexistent_tool'));
|
|
1502
|
-
});
|
|
1503
|
-
|
|
1504
|
-
test('anthropic(-oauth) leak guard: benign prose <function> mention preserved, not swallowed', async () => {
|
|
1505
|
-
const prose = 'Use the <function> keyword in JavaScript to declare a function.';
|
|
1506
|
-
const texts = [];
|
|
1507
|
-
const captured = [];
|
|
1508
|
-
const result = await anthropicParseSSEStream(
|
|
1509
|
-
anthropicSseResponse(textDeltaEvents([prose])),
|
|
1510
|
-
null, () => {}, () => {},
|
|
1511
|
-
(call) => captured.push(call),
|
|
1512
|
-
{},
|
|
1513
|
-
(t) => texts.push(t),
|
|
1514
|
-
LEAK_TOOLS,
|
|
1515
|
-
);
|
|
1516
|
-
assert.equal(result.toolCalls, undefined);
|
|
1517
|
-
assert.equal(captured.length, 0);
|
|
1518
|
-
assert.equal(texts.join(''), prose);
|
|
1519
|
-
assert.equal(result.content, prose);
|
|
1520
|
-
});
|
|
1521
|
-
|
|
1522
|
-
// === 4. openai-oauth / openai-oauth-ws =====================================
|
|
1523
|
-
// openai-oauth (HTTP/SSE) and openai-oauth-ws (WebSocket) both consume the
|
|
1524
|
-
// Responses event stream inside large stateful stream loops, NOT a standalone
|
|
1525
|
-
// parser. The HTTP path's handleEvent is a private closure inside
|
|
1526
|
-
// sendViaHttpSse (openai-oauth.mjs:1038) and cannot be exported without
|
|
1527
|
-
// extracting it (forbidden: no logic change). The WS path's _streamResponse
|
|
1528
|
-
// (openai-oauth-ws.mjs:1190) IS exported but requires a live `entry.socket`
|
|
1529
|
-
// EventEmitter and resolves only on response.completed — driving it needs a
|
|
1530
|
-
// full fake-socket test rig, well beyond "inject synthetic input to a parser".
|
|
1531
|
-
//
|
|
1532
|
-
// Their canonical Responses function_call shape (call_id/name/arguments) and
|
|
1533
|
-
// custom_tool_call handling are the SAME wire contract already asserted via
|
|
1534
|
-
// openai-compat's parseResponsesToolCalls above, and the shared
|
|
1535
|
-
// customToolCallFromResponseItem helper (custom-tool-wire.mjs) is imported by
|
|
1536
|
-
// all three. We add a focused unit test for that shared custom-tool helper so
|
|
1537
|
-
// the OAuth custom_tool_call extraction path has explicit coverage; the
|
|
1538
|
-
// function_call path is covered by the openai-compat Responses test.
|
|
1539
|
-
|
|
1540
|
-
import { customToolCallFromResponseItem } from '../src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
|
|
1541
|
-
import { parseToolSearchArgs } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
1542
|
-
import { _warmupContinuityTraceForTest } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
1543
|
-
|
|
1544
|
-
test('openai-oauth-ws (warmup continuity): warmup id anchors first real; first-3 misses counted', () => {
|
|
1545
|
-
const t = _warmupContinuityTraceForTest({
|
|
1546
|
-
warmupUsed: true, warmupResponseId: 'resp_w', priorEntryResponseId: 'resp_w',
|
|
1547
|
-
sentPrevResponseId: null, earlyCacheMisses: [false, 'warm_session_zero_cached_tokens', false, true],
|
|
1548
|
-
});
|
|
1549
|
-
assert.equal(t.warmup_chain_continuous, true);
|
|
1550
|
-
assert.equal(t.warmup_first_real_prev_id, 'resp_w');
|
|
1551
|
-
assert.deepEqual(t.early_cache_misses, [false, 'warm_session_zero_cached_tokens', false]);
|
|
1552
|
-
assert.equal(t.early_cache_miss_count, 1);
|
|
1553
|
-
assert.equal(_warmupContinuityTraceForTest({ warmupUsed: true, warmupResponseId: 'a', priorEntryResponseId: 'b' }).warmup_chain_continuous, false);
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
test('openai-oauth (shared custom-tool-wire): custom_tool_call item → canonical call', () => {
|
|
1557
|
-
const item = { type: 'custom_tool_call', call_id: 'ctc_1', name: 'apply_patch', input: '*** patch ***' };
|
|
1558
|
-
const call = customToolCallFromResponseItem(item);
|
|
1559
|
-
assert.equal(call.id, 'ctc_1');
|
|
1560
|
-
assert.equal(call.name, 'apply_patch');
|
|
1561
|
-
assert.deepEqual(call.arguments, { patch: '*** patch ***' });
|
|
1562
|
-
assert.equal(call.nativeType, 'custom_tool_call');
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
test('openai-oauth (shared custom-tool-wire): non custom_tool_call → null', () => {
|
|
1566
|
-
assert.equal(customToolCallFromResponseItem({ type: 'function_call' }), null);
|
|
1567
|
-
assert.equal(customToolCallFromResponseItem(null), null);
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
// WS tool_search_call.arguments parse policy (parseToolSearchArgs, module-scope
|
|
1571
|
-
// export of openai-oauth-ws.mjs). Native convergence: malformed non-empty JSON
|
|
1572
|
-
// becomes an invalid-args marker so the dispatch loop blocks execution and
|
|
1573
|
-
// returns an is_error tool_result — NOT a silent {} that would dispatch
|
|
1574
|
-
// tool_search with empty arguments. Empty/whitespace/object inputs keep their
|
|
1575
|
-
// prior, correct behavior.
|
|
1576
|
-
test('openai-oauth-ws (tool_search): malformed args string → invalid-args marker (not {})', () => {
|
|
1577
|
-
const out = parseToolSearchArgs('{"query": dispatchAiWrapped}');
|
|
1578
|
-
assert.equal(isInvalidToolArgsMarker(out), true);
|
|
1579
|
-
assert.equal(out.__invalidToolArgs, true);
|
|
1580
|
-
assert.equal(out.__rawArguments, '{"query": dispatchAiWrapped}');
|
|
1581
|
-
assert.equal(typeof out.__parseError, 'string');
|
|
1582
|
-
assert.ok(out.__parseError.length > 0);
|
|
1583
|
-
});
|
|
1584
|
-
|
|
1585
|
-
test('openai-oauth-ws (tool_search): valid args / object / empty preserved', () => {
|
|
1586
|
-
// valid JSON string → parsed object
|
|
1587
|
-
assert.deepEqual(parseToolSearchArgs('{"query":"x"}'), { query: 'x' });
|
|
1588
|
-
// already an object → passthrough
|
|
1589
|
-
const obj = { query: 'y' };
|
|
1590
|
-
assert.equal(parseToolSearchArgs(obj), obj);
|
|
1591
|
-
// empty / whitespace / null / non-string → {} (no args, not a marker)
|
|
1592
|
-
assert.deepEqual(parseToolSearchArgs(''), {});
|
|
1593
|
-
assert.deepEqual(parseToolSearchArgs(' '), {});
|
|
1594
|
-
assert.deepEqual(parseToolSearchArgs(null), {});
|
|
1595
|
-
assert.deepEqual(parseToolSearchArgs(undefined), {});
|
|
1596
|
-
assert.equal(isInvalidToolArgsMarker(parseToolSearchArgs('')), false);
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
// === 5. grok-oauth =========================================================
|
|
1600
|
-
// grok-oauth has NO independent tool_call parser. GrokOAuthProvider delegates
|
|
1601
|
-
// all request shaping AND response parsing to an inner OpenAICompatProvider
|
|
1602
|
-
// constructed as `new OpenAICompatProvider('xai', ...)` (grok-oauth.mjs:668).
|
|
1603
|
-
// Its tool_call extraction therefore goes through the exact
|
|
1604
|
-
// parseToolCalls / parseResponsesToolCalls already asserted in block 1 — no
|
|
1605
|
-
// duplicate test. (Documented as shared in the report.)
|
|
1606
|
-
|
|
1607
|
-
// === 6. OpenAI leaked tool-call recovery ===================================
|
|
1608
|
-
// The model sometimes emits a tool call as PLAIN TEXT (XML `<invoke>` family
|
|
1609
|
-
// or gpt-oss harmony `<|channel|>...to=functions.NAME...<|call|>`) inside a
|
|
1610
|
-
// text delta instead of a native structured tool_call. The stream guards
|
|
1611
|
-
// suppress the tags from the visible stream, synthesize a native-shaped call
|
|
1612
|
-
// (`call_leaked_*` id), and dispatch it via the same onToolCall path.
|
|
1613
|
-
const OAI_LEAK_TOOLS = new Set(['shell', 'read']);
|
|
1614
|
-
|
|
1615
|
-
function chatCompletionStream(contentChunks) {
|
|
1616
|
-
// Each chunk is an assistant text delta; ends with a stop finish_reason.
|
|
1617
|
-
const events = contentChunks.map((text) => ({
|
|
1618
|
-
choices: [{ delta: { content: text } }],
|
|
1619
|
-
}));
|
|
1620
|
-
events.push({ choices: [{ delta: {}, finish_reason: 'stop' }], usage: { total_tokens: 1 } });
|
|
1621
|
-
return compatResponsesEventStream(events);
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
function responsesTextStream(textChunks) {
|
|
1625
|
-
const events = textChunks.map((delta) => ({ type: 'response.output_text.delta', delta }));
|
|
1626
|
-
events.push({ type: 'response.completed', response: { id: 'r1', model: 'gpt', status: 'completed', output: [] } });
|
|
1627
|
-
return compatResponsesEventStream(events);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
test('openai-compat (chat) leak guard: leaked <invoke> for known tool → recovered, no leak', async () => {
|
|
1631
|
-
const leaked = 'Sure.\n<function_calls>\n<invoke name="shell">\n<parameter name="command">ls -la</parameter>\n</invoke>\n</function_calls>';
|
|
1632
|
-
const texts = [];
|
|
1633
|
-
const captured = [];
|
|
1634
|
-
const out = await consumeCompatChatCompletionStream(chatCompletionStream([leaked]), {
|
|
1635
|
-
label: 'test',
|
|
1636
|
-
parseToolCalls: compatParseToolCalls,
|
|
1637
|
-
onToolCall: (call) => captured.push(call),
|
|
1638
|
-
onTextDelta: (t) => texts.push(t),
|
|
1639
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1640
|
-
});
|
|
1641
|
-
const emitted = texts.join('');
|
|
1642
|
-
assert.equal(/<invoke|<function_calls|<parameter/.test(emitted), false);
|
|
1643
|
-
assert.equal(/<invoke|<function_calls|<parameter/.test(out.content), false);
|
|
1644
|
-
assert.ok(emitted.includes('Sure.'));
|
|
1645
|
-
assert.ok(out.content.includes('Sure.'));
|
|
1646
|
-
assert.equal(out.toolCalls.length, 1);
|
|
1647
|
-
assert.equal(out.toolCalls[0].name, 'shell');
|
|
1648
|
-
assert.deepEqual(out.toolCalls[0].arguments, { command: 'ls -la' });
|
|
1649
|
-
assert.match(out.toolCalls[0].id, /^call_leaked_/);
|
|
1650
|
-
assert.equal(captured.length, 1);
|
|
1651
|
-
assert.equal(captured[0].name, 'shell');
|
|
1652
|
-
assert.deepEqual(captured[0].arguments, { command: 'ls -la' });
|
|
1653
|
-
});
|
|
1654
|
-
|
|
1655
|
-
test('openai-compat (chat) leak guard: leaked harmony <|channel|>...<|call|> for known tool → recovered', async () => {
|
|
1656
|
-
const leaked = '<|channel|>commentary to=functions.read <|constrain|>json<|message|>{"path":"a.txt"}<|call|>';
|
|
1657
|
-
const texts = [];
|
|
1658
|
-
const captured = [];
|
|
1659
|
-
const out = await consumeCompatChatCompletionStream(chatCompletionStream(['ok ', leaked]), {
|
|
1660
|
-
label: 'test',
|
|
1661
|
-
parseToolCalls: compatParseToolCalls,
|
|
1662
|
-
onToolCall: (call) => captured.push(call),
|
|
1663
|
-
onTextDelta: (t) => texts.push(t),
|
|
1664
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1665
|
-
});
|
|
1666
|
-
assert.equal(/<\|channel\|>|to=functions|<\|call\|>/.test(texts.join('')), false);
|
|
1667
|
-
assert.equal(/<\|channel\|>|<\|call\|>/.test(out.content), false);
|
|
1668
|
-
assert.equal(out.toolCalls.length, 1);
|
|
1669
|
-
assert.equal(out.toolCalls[0].name, 'read');
|
|
1670
|
-
assert.deepEqual(out.toolCalls[0].arguments, { path: 'a.txt' });
|
|
1671
|
-
assert.equal(captured.length, 1);
|
|
1672
|
-
assert.equal(captured[0].name, 'read');
|
|
1673
|
-
});
|
|
1674
|
-
|
|
1675
|
-
test('openai-compat (chat) leak guard: XML tags split across two content deltas still detected', async () => {
|
|
1676
|
-
const a = '<invoke name="sh';
|
|
1677
|
-
const b = 'ell">\n<parameter name="command">pwd</parameter>\n</invoke>';
|
|
1678
|
-
const texts = [];
|
|
1679
|
-
const captured = [];
|
|
1680
|
-
const out = await consumeCompatChatCompletionStream(chatCompletionStream([a, b]), {
|
|
1681
|
-
label: 'test',
|
|
1682
|
-
parseToolCalls: compatParseToolCalls,
|
|
1683
|
-
onToolCall: (call) => captured.push(call),
|
|
1684
|
-
onTextDelta: (t) => texts.push(t),
|
|
1685
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1686
|
-
});
|
|
1687
|
-
assert.equal(/<invoke|<parameter/.test(texts.join('')), false);
|
|
1688
|
-
assert.equal(out.toolCalls.length, 1);
|
|
1689
|
-
assert.equal(out.toolCalls[0].name, 'shell');
|
|
1690
|
-
assert.deepEqual(out.toolCalls[0].arguments, { command: 'pwd' });
|
|
1691
|
-
assert.deepEqual(captured[0].arguments, { command: 'pwd' });
|
|
1692
|
-
});
|
|
1693
|
-
|
|
1694
|
-
test('openai-compat (chat) leak guard: unknown tool → text preserved, no synthetic call', async () => {
|
|
1695
|
-
const leaked = '<function_calls>\n<invoke name="nonexistent_tool">\n<parameter name="x">1</parameter>\n</invoke>\n</function_calls>';
|
|
1696
|
-
const texts = [];
|
|
1697
|
-
const captured = [];
|
|
1698
|
-
const out = await consumeCompatChatCompletionStream(chatCompletionStream([leaked]), {
|
|
1699
|
-
label: 'test',
|
|
1700
|
-
parseToolCalls: compatParseToolCalls,
|
|
1701
|
-
onToolCall: (call) => captured.push(call),
|
|
1702
|
-
onTextDelta: (t) => texts.push(t),
|
|
1703
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1704
|
-
});
|
|
1705
|
-
assert.equal(out.toolCalls, undefined);
|
|
1706
|
-
assert.equal(captured.length, 0);
|
|
1707
|
-
assert.ok(texts.join('').includes('nonexistent_tool'));
|
|
1708
|
-
assert.ok(out.content.includes('nonexistent_tool'));
|
|
1709
|
-
});
|
|
1710
|
-
|
|
1711
|
-
test('openai-compat (chat) leak guard: benign prose preserved, native tool_calls path intact', async () => {
|
|
1712
|
-
const prose = 'Use the <function> keyword in JavaScript.';
|
|
1713
|
-
const texts = [];
|
|
1714
|
-
const captured = [];
|
|
1715
|
-
const out = await consumeCompatChatCompletionStream(chatCompletionStream([prose]), {
|
|
1716
|
-
label: 'test',
|
|
1717
|
-
parseToolCalls: compatParseToolCalls,
|
|
1718
|
-
onToolCall: (call) => captured.push(call),
|
|
1719
|
-
onTextDelta: (t) => texts.push(t),
|
|
1720
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1721
|
-
});
|
|
1722
|
-
assert.equal(out.toolCalls, undefined);
|
|
1723
|
-
assert.equal(captured.length, 0);
|
|
1724
|
-
assert.equal(texts.join(''), prose);
|
|
1725
|
-
assert.equal(out.content, prose);
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
test('openai-compat (chat) leak guard: native structured tool_calls still work with guard enabled', async () => {
|
|
1729
|
-
const captured = [];
|
|
1730
|
-
const events = [
|
|
1731
|
-
{ choices: [{ delta: { tool_calls: [{ index: 0, id: 'call_1', type: 'function', function: { name: 'read', arguments: '{"path":"x"}' } }] } }] },
|
|
1732
|
-
{ choices: [{ delta: {}, finish_reason: 'tool_calls' }], usage: { total_tokens: 1 } },
|
|
1733
|
-
];
|
|
1734
|
-
const out = await consumeCompatChatCompletionStream(compatResponsesEventStream(events), {
|
|
1735
|
-
label: 'test',
|
|
1736
|
-
parseToolCalls: compatParseToolCalls,
|
|
1737
|
-
onToolCall: (call) => captured.push(call),
|
|
1738
|
-
onTextDelta: () => {},
|
|
1739
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1740
|
-
});
|
|
1741
|
-
assert.deepEqual(out.toolCalls, [{ id: 'call_1', name: 'read', arguments: { path: 'x' } }]);
|
|
1742
|
-
assert.deepEqual(captured, [{ id: 'call_1', name: 'read', arguments: { path: 'x' } }]);
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
test('openai-compat (responses) leak guard: leaked <invoke> in output_text.delta → recovered', async () => {
|
|
1746
|
-
const leaked = 'Working.\n<invoke name="shell">\n<parameter name="command">whoami</parameter>\n</invoke>';
|
|
1747
|
-
const texts = [];
|
|
1748
|
-
const captured = [];
|
|
1749
|
-
const out = await consumeCompatResponsesStream(responsesTextStream([leaked]), {
|
|
1750
|
-
label: 'test',
|
|
1751
|
-
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
1752
|
-
responseOutputText: () => '',
|
|
1753
|
-
onToolCall: (call) => captured.push(call),
|
|
1754
|
-
onTextDelta: (t) => texts.push(t),
|
|
1755
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1756
|
-
});
|
|
1757
|
-
assert.equal(/<invoke|<parameter/.test(texts.join('')), false);
|
|
1758
|
-
assert.equal(/<invoke|<parameter/.test(out.content), false);
|
|
1759
|
-
assert.ok(out.content.includes('Working.'));
|
|
1760
|
-
assert.equal(out.toolCalls.length, 1);
|
|
1761
|
-
assert.equal(out.toolCalls[0].name, 'shell');
|
|
1762
|
-
assert.deepEqual(out.toolCalls[0].arguments, { command: 'whoami' });
|
|
1763
|
-
assert.match(out.toolCalls[0].id, /^call_leaked_/);
|
|
1764
|
-
assert.equal(captured.length, 1);
|
|
1765
|
-
});
|
|
1766
|
-
|
|
1767
|
-
test('openai-compat (responses) leak guard: harmony syntax split across two deltas → recovered', async () => {
|
|
1768
|
-
const a = '<|channel|>commentary to=functions.read <|message|>{"path":';
|
|
1769
|
-
const b = '"b.txt"}<|call|>';
|
|
1770
|
-
const captured = [];
|
|
1771
|
-
const out = await consumeCompatResponsesStream(responsesTextStream([a, b]), {
|
|
1772
|
-
label: 'test',
|
|
1773
|
-
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
1774
|
-
responseOutputText: () => '',
|
|
1775
|
-
onToolCall: (call) => captured.push(call),
|
|
1776
|
-
onTextDelta: () => {},
|
|
1777
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1778
|
-
});
|
|
1779
|
-
assert.equal(out.toolCalls.length, 1);
|
|
1780
|
-
assert.equal(out.toolCalls[0].name, 'read');
|
|
1781
|
-
assert.deepEqual(out.toolCalls[0].arguments, { path: 'b.txt' });
|
|
1782
|
-
assert.equal(captured.length, 1);
|
|
1783
|
-
});
|
|
1784
|
-
|
|
1785
|
-
test('openai-compat (responses) leak guard: benign prose preserved, no synthetic call', async () => {
|
|
1786
|
-
const prose = 'Just some prose about functions and channels.';
|
|
1787
|
-
const captured = [];
|
|
1788
|
-
const out = await consumeCompatResponsesStream(responsesTextStream([prose]), {
|
|
1789
|
-
label: 'test',
|
|
1790
|
-
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
1791
|
-
responseOutputText: () => '',
|
|
1792
|
-
onToolCall: (call) => captured.push(call),
|
|
1793
|
-
onTextDelta: () => {},
|
|
1794
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1795
|
-
});
|
|
1796
|
-
assert.equal(out.toolCalls, undefined);
|
|
1797
|
-
assert.equal(captured.length, 0);
|
|
1798
|
-
assert.ok(out.content.includes('Just some prose'));
|
|
1799
|
-
});
|
|
1800
|
-
|
|
1801
|
-
// === 7. Reviewer fixes: fence gating, cross-path dedupe, bare-antml ========
|
|
1802
|
-
|
|
1803
|
-
// --- Fix 1: code-fence / inline-code gating (Anthropic path) ---------------
|
|
1804
|
-
// A complete <invoke> written inside a ```code fence``` or inline `code` span
|
|
1805
|
-
// is a documentation example, not a real call: it must stream as visible text
|
|
1806
|
-
// and NOT dispatch. The control (same tag OUTSIDE a fence) still recovers.
|
|
1807
|
-
test('anthropic leak guard (fence): <invoke> inside a fenced code block → emitted as text, NOT dispatched', async () => {
|
|
1808
|
-
const fenced = 'Example:\n```\n<invoke name="read"><parameter name="path">a</parameter></invoke>\n```\ndone';
|
|
1809
|
-
const texts = [];
|
|
1810
|
-
const captured = [];
|
|
1811
|
-
const result = await anthropicParseSSEStream(
|
|
1812
|
-
anthropicSseResponse(textDeltaEvents([fenced])),
|
|
1813
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, (t) => texts.push(t), LEAK_TOOLS,
|
|
1814
|
-
);
|
|
1815
|
-
assert.equal(result.toolCalls, undefined);
|
|
1816
|
-
assert.equal(captured.length, 0);
|
|
1817
|
-
assert.ok(texts.join('').includes('<invoke name="read">'));
|
|
1818
|
-
assert.ok(result.content.includes('<invoke name="read">'));
|
|
1819
|
-
});
|
|
1820
|
-
|
|
1821
|
-
test('anthropic leak guard (fence): fence OPENS in one delta and CLOSES in a later one, tag between → NOT dispatched', async () => {
|
|
1822
|
-
const chunks = ['Here is an example:\n```json\n', '<invoke name="read"><parameter name="path">a</parameter></invoke>\n', '```\nEnd.'];
|
|
1823
|
-
const texts = [];
|
|
1824
|
-
const captured = [];
|
|
1825
|
-
const result = await anthropicParseSSEStream(
|
|
1826
|
-
anthropicSseResponse(textDeltaEvents(chunks)),
|
|
1827
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, (t) => texts.push(t), LEAK_TOOLS,
|
|
1828
|
-
);
|
|
1829
|
-
assert.equal(result.toolCalls, undefined);
|
|
1830
|
-
assert.equal(captured.length, 0);
|
|
1831
|
-
assert.ok(texts.join('').includes('<invoke name="read">'));
|
|
1832
|
-
});
|
|
1833
|
-
|
|
1834
|
-
test('anthropic leak guard (fence): <invoke> inside an inline `code` span → NOT dispatched', async () => {
|
|
1835
|
-
const inline = 'Call it like `<invoke name="read"><parameter name="path">a</parameter></invoke>` in the docs.';
|
|
1836
|
-
const texts = [];
|
|
1837
|
-
const captured = [];
|
|
1838
|
-
const result = await anthropicParseSSEStream(
|
|
1839
|
-
anthropicSseResponse(textDeltaEvents([inline])),
|
|
1840
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, (t) => texts.push(t), LEAK_TOOLS,
|
|
1841
|
-
);
|
|
1842
|
-
assert.equal(result.toolCalls, undefined);
|
|
1843
|
-
assert.equal(captured.length, 0);
|
|
1844
|
-
assert.ok(texts.join('').includes('<invoke name="read">'));
|
|
1845
|
-
});
|
|
1846
|
-
|
|
1847
|
-
test('anthropic leak guard (fence CONTROL): same <invoke> OUTSIDE any fence → still recovered/dispatched', async () => {
|
|
1848
|
-
const outside = 'Sure.\n<invoke name="read"><parameter name="path">a</parameter></invoke>';
|
|
1849
|
-
const texts = [];
|
|
1850
|
-
const captured = [];
|
|
1851
|
-
const result = await anthropicParseSSEStream(
|
|
1852
|
-
anthropicSseResponse(textDeltaEvents([outside])),
|
|
1853
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, (t) => texts.push(t), LEAK_TOOLS,
|
|
1854
|
-
);
|
|
1855
|
-
assert.equal(/<invoke|<parameter/.test(texts.join('')), false);
|
|
1856
|
-
assert.equal(result.toolCalls.length, 1);
|
|
1857
|
-
assert.equal(result.toolCalls[0].name, 'read');
|
|
1858
|
-
assert.deepEqual(result.toolCalls[0].arguments, { path: 'a' });
|
|
1859
|
-
assert.equal(captured.length, 1);
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
// --- Fix 1: harmony example inside a fence (OpenAI chat path) → NOT dispatched
|
|
1863
|
-
test('openai-compat (chat) leak guard (fence): harmony example inside a ```fence``` → emitted as text, NOT dispatched', async () => {
|
|
1864
|
-
const fenced = 'Doc:\n```\n<|channel|>commentary to=functions.read <|message|>{"path":"a"}<|call|>\n```\n';
|
|
1865
|
-
const texts = [];
|
|
1866
|
-
const captured = [];
|
|
1867
|
-
const out = await consumeCompatChatCompletionStream(chatCompletionStream([fenced]), {
|
|
1868
|
-
label: 'test',
|
|
1869
|
-
parseToolCalls: compatParseToolCalls,
|
|
1870
|
-
onToolCall: (call) => captured.push(call),
|
|
1871
|
-
onTextDelta: (t) => texts.push(t),
|
|
1872
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1873
|
-
});
|
|
1874
|
-
assert.equal(out.toolCalls, undefined);
|
|
1875
|
-
assert.equal(captured.length, 0);
|
|
1876
|
-
assert.ok(texts.join('').includes('to=functions.read'));
|
|
1877
|
-
});
|
|
1878
|
-
|
|
1879
|
-
// --- Fix 2: duplicate dispatch (text-leaked + identical native) → ONE fire --
|
|
1880
|
-
test('anthropic leak guard (dedupe): text-leaked call + identical native tool_use → onToolCall fires ONCE', async () => {
|
|
1881
|
-
const leaked = '<invoke name="shell"><parameter name="command">ls</parameter></invoke>';
|
|
1882
|
-
const events = [
|
|
1883
|
-
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
1884
|
-
{ type: 'content_block_start', index: 0, content_block: { type: 'text' } },
|
|
1885
|
-
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: leaked } },
|
|
1886
|
-
{ type: 'content_block_stop', index: 0 },
|
|
1887
|
-
{ type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_1', name: 'shell' } },
|
|
1888
|
-
{ type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"command":"ls"}' } },
|
|
1889
|
-
{ type: 'content_block_stop', index: 1 },
|
|
1890
|
-
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
1891
|
-
{ type: 'message_stop' },
|
|
1892
|
-
];
|
|
1893
|
-
const captured = [];
|
|
1894
|
-
const result = await anthropicParseSSEStream(
|
|
1895
|
-
anthropicSseResponse(events),
|
|
1896
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, () => {}, LEAK_TOOLS,
|
|
1897
|
-
);
|
|
1898
|
-
// Exactly one dispatch for the identical (name,args) fingerprint.
|
|
1899
|
-
assert.equal(captured.length, 1);
|
|
1900
|
-
assert.equal(captured[0].name, 'shell');
|
|
1901
|
-
assert.deepEqual(captured[0].arguments, { command: 'ls' });
|
|
1902
|
-
// ...and the RETURNED array carries exactly one — else the loop would
|
|
1903
|
-
// execute the side-effecting tool twice (Fix 2 array side).
|
|
1904
|
-
assert.equal(result.toolCalls.length, 1);
|
|
1905
|
-
});
|
|
1906
|
-
|
|
1907
|
-
test('openai-compat (chat) leak guard (dedupe): text-leaked call + identical native tool_calls → onToolCall fires ONCE', async () => {
|
|
1908
|
-
const leaked = '<invoke name="read"><parameter name="path">a</parameter></invoke>';
|
|
1909
|
-
const events = [
|
|
1910
|
-
{ choices: [{ delta: { content: leaked } }] },
|
|
1911
|
-
{ choices: [{ delta: { tool_calls: [{ index: 0, id: 'call_1', type: 'function', function: { name: 'read', arguments: '{"path":"a"}' } }] } }] },
|
|
1912
|
-
{ choices: [{ delta: {}, finish_reason: 'tool_calls' }], usage: { total_tokens: 1 } },
|
|
1913
|
-
];
|
|
1914
|
-
const captured = [];
|
|
1915
|
-
const out = await consumeCompatChatCompletionStream(compatResponsesEventStream(events), {
|
|
1916
|
-
label: 'test',
|
|
1917
|
-
parseToolCalls: compatParseToolCalls,
|
|
1918
|
-
onToolCall: (call) => captured.push(call),
|
|
1919
|
-
onTextDelta: () => {},
|
|
1920
|
-
knownToolNames: OAI_LEAK_TOOLS,
|
|
1921
|
-
});
|
|
1922
|
-
assert.equal(captured.length, 1);
|
|
1923
|
-
assert.equal(captured[0].name, 'read');
|
|
1924
|
-
assert.deepEqual(captured[0].arguments, { path: 'a' });
|
|
1925
|
-
// The RETURNED array is deduped too (Fix 2 array side): the agent loop
|
|
1926
|
-
// executes returned toolCalls, so a synthetic+native duplicate here would
|
|
1927
|
-
// run the side-effecting tool twice. Exactly one must survive.
|
|
1928
|
-
assert.equal(out.toolCalls.length, 1);
|
|
1929
|
-
assert.equal(out.toolCalls[0].name, 'read');
|
|
1930
|
-
});
|
|
1931
|
-
|
|
1932
|
-
// --- Fix 3: bare `antml:invoke` in prose (no `<`) → streamed, not held ------
|
|
1933
|
-
test('anthropic leak guard (bare-antml): literal "antml:invoke" in prose → streamed promptly, no dispatch', async () => {
|
|
1934
|
-
const prose = 'The tag antml:invoke is used internally; here we just mention it in a sentence.';
|
|
1935
|
-
const texts = [];
|
|
1936
|
-
const captured = [];
|
|
1937
|
-
const result = await anthropicParseSSEStream(
|
|
1938
|
-
anthropicSseResponse(textDeltaEvents([prose])),
|
|
1939
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, (t) => texts.push(t), LEAK_TOOLS,
|
|
1940
|
-
);
|
|
1941
|
-
assert.equal(result.toolCalls, undefined);
|
|
1942
|
-
assert.equal(captured.length, 0);
|
|
1943
|
-
// Streamed promptly (not held to final) and content intact.
|
|
1944
|
-
assert.equal(texts.join(''), prose);
|
|
1945
|
-
assert.equal(result.content, prose);
|
|
1946
|
-
});
|
|
1947
|
-
|
|
1948
|
-
test('anthropic leak guard (bare-antml CONTROL): <invoke> bracket form still recovers', async () => {
|
|
1949
|
-
const leaked = 'ok\n<invoke name="read"><parameter name="path">z</parameter></invoke>';
|
|
1950
|
-
const captured = [];
|
|
1951
|
-
const result = await anthropicParseSSEStream(
|
|
1952
|
-
anthropicSseResponse(textDeltaEvents([leaked])),
|
|
1953
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, () => {}, LEAK_TOOLS,
|
|
1954
|
-
);
|
|
1955
|
-
assert.equal(result.toolCalls.length, 1);
|
|
1956
|
-
assert.equal(captured.length, 1);
|
|
1957
|
-
assert.equal(captured[0].name, 'read');
|
|
1958
|
-
assert.deepEqual(captured[0].arguments, { path: 'z' });
|
|
1959
|
-
});
|
|
1960
|
-
|
|
1961
|
-
// --- Reviewer Medium: suppressed call args must NOT poison fence state -------
|
|
1962
|
-
// A recovered/suppressed leaked call whose args contain an unmatched backtick
|
|
1963
|
-
// must not leave the markdown fence "open" and wrongly suppress a LATER real
|
|
1964
|
-
// leaked call as if it were inside a code span.
|
|
1965
|
-
test('anthropic leak guard (fence): unmatched backtick inside a suppressed call\'s args does not swallow a later real call', async () => {
|
|
1966
|
-
// First leaked call carries a lone backtick in its arg value; second is a
|
|
1967
|
-
// clean leaked call that MUST still be recovered.
|
|
1968
|
-
const first = '<invoke name="read"><parameter name="path">a`b</parameter></invoke>';
|
|
1969
|
-
const between = ' some visible prose ';
|
|
1970
|
-
const second = '<invoke name="shell"><parameter name="command">ls</parameter></invoke>';
|
|
1971
|
-
const texts = [];
|
|
1972
|
-
const captured = [];
|
|
1973
|
-
const result = await anthropicParseSSEStream(
|
|
1974
|
-
anthropicSseResponse(textDeltaEvents([first + between + second])),
|
|
1975
|
-
null, () => {}, () => {}, (c) => captured.push(c), {}, (t) => texts.push(t), LEAK_TOOLS,
|
|
1976
|
-
);
|
|
1977
|
-
// BOTH leaked calls recovered — the stray backtick in call #1's args did
|
|
1978
|
-
// not open a fence that suppressed call #2.
|
|
1979
|
-
assert.equal(captured.length, 2);
|
|
1980
|
-
assert.equal(result.toolCalls.length, 2);
|
|
1981
|
-
assert.deepEqual(captured.map((c) => c.name), ['read', 'shell']);
|
|
1982
|
-
// The in-between prose streamed; the tag/arg text did not leak.
|
|
1983
|
-
assert.equal(texts.join('').includes('some visible prose'), true);
|
|
1984
|
-
assert.equal(texts.join('').includes('<invoke'), false);
|
|
1985
|
-
assert.equal(texts.join('').includes('`'), false);
|
|
1986
|
-
});
|
|
1987
|
-
|
|
1988
|
-
// === 8. Anthropic effort (output_config vs legacy thinking budget) ==========
|
|
1989
|
-
|
|
1990
|
-
test('anthropic effort: sonnet-4-6 uses output_config + effort beta, not thinking', () => {
|
|
1991
|
-
const model = 'claude-sonnet-4-6';
|
|
1992
|
-
const body = _buildRequestBodyForCacheSmoke(
|
|
1993
|
-
[{ role: 'user', content: 'hi' }],
|
|
1994
|
-
model,
|
|
1995
|
-
[],
|
|
1996
|
-
{ effort: 'high' },
|
|
1997
|
-
);
|
|
1998
|
-
assert.deepEqual(body.output_config, { effort: 'high' });
|
|
1999
|
-
// Adaptive-thinking models also carry thinking:{type:'adaptive'} — the
|
|
2000
|
-
// legacy budget_tokens shape 400s on these models.
|
|
2001
|
-
assert.deepEqual(body.thinking, { type: 'adaptive', display: 'summarized' });
|
|
2002
|
-
assert.equal(shouldIncludeEffortBeta(model, { effort: 'high' }), true);
|
|
2003
|
-
const beta = buildAnthropicBetaHeaders({ effort: true });
|
|
2004
|
-
assert.ok(beta.includes(EFFORT_BETA_HEADER));
|
|
2005
|
-
});
|
|
2006
|
-
|
|
2007
|
-
test('anthropic-oauth: foreign native references normalize to ordinary tool_result after provider switches', () => {
|
|
2008
|
-
const body = _buildRequestBodyForCacheSmoke(
|
|
2009
|
-
[
|
|
2010
|
-
{ role: 'user', content: 'load a tool' },
|
|
2011
|
-
{
|
|
2012
|
-
role: 'assistant',
|
|
2013
|
-
content: '',
|
|
2014
|
-
toolCalls: [{ id: 'toolu_load_1', name: 'load_tool', arguments: { names: ['read'] } }],
|
|
2015
|
-
},
|
|
2016
|
-
{
|
|
2017
|
-
role: 'tool',
|
|
2018
|
-
toolCallId: 'toolu_load_1',
|
|
2019
|
-
content: '{"loaded":["read"]}',
|
|
2020
|
-
nativeToolSearch: { provider: 'openai-oauth', toolReferences: ['read'] },
|
|
2021
|
-
},
|
|
2022
|
-
],
|
|
2023
|
-
'claude-opus-4-8',
|
|
2024
|
-
[],
|
|
2025
|
-
{ effort: 'high' },
|
|
2026
|
-
);
|
|
2027
|
-
const serialized = JSON.stringify(body.messages);
|
|
2028
|
-
assert.equal(serialized.includes('tool_reference'), false);
|
|
2029
|
-
const toolResult = body.messages
|
|
2030
|
-
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
2031
|
-
.find((block) => block?.type === 'tool_result' && block.tool_use_id === 'toolu_load_1');
|
|
2032
|
-
assert.ok(toolResult);
|
|
2033
|
-
assert.equal(toolResult.content, '{"loaded":["read"]}');
|
|
2034
|
-
});
|
|
2035
|
-
|
|
2036
|
-
test('anthropic effort: legacy sonnet-4-5 maps effort to thinking budget', () => {
|
|
2037
|
-
const model = 'claude-sonnet-4-5-20250514';
|
|
2038
|
-
const body = _buildRequestBodyForCacheSmoke(
|
|
2039
|
-
[{ role: 'user', content: 'hi' }],
|
|
2040
|
-
model,
|
|
2041
|
-
[],
|
|
2042
|
-
{ effort: 'medium' },
|
|
2043
|
-
);
|
|
2044
|
-
assert.equal(body.output_config, undefined);
|
|
2045
|
-
assert.deepEqual(body.thinking, { type: 'enabled', budget_tokens: LEGACY_EFFORT_BUDGET.medium });
|
|
2046
|
-
assert.equal(modelSupportsEffort(model), false);
|
|
2047
|
-
});
|
|
2048
|
-
|
|
2049
|
-
test('anthropic effort: xhigh on opus-4-8 is a first-class level (not downgraded to max)', () => {
|
|
2050
|
-
// Opus 4.8 supports xhigh (modelSupportsXhighEffort), so xhigh is kept
|
|
2051
|
-
// verbatim as a first-class effort level — matching codex's ReasoningEffort
|
|
2052
|
-
// enum which lists xhigh between high and max. Only models WITHOUT xhigh
|
|
2053
|
-
// support clamp it down to high.
|
|
2054
|
-
const model = 'claude-opus-4-8';
|
|
2055
|
-
assert.equal(modelSupportsMaxEffort(model), true);
|
|
2056
|
-
assert.equal(normalizeAnthropicEffortInput('xhigh', model), 'xhigh');
|
|
2057
|
-
const body = _buildRequestBodyForCacheSmoke(
|
|
2058
|
-
[{ role: 'user', content: 'hi' }],
|
|
2059
|
-
model,
|
|
2060
|
-
[],
|
|
2061
|
-
{ effort: 'xhigh' },
|
|
2062
|
-
);
|
|
2063
|
-
assert.deepEqual(body.output_config, { effort: 'xhigh' });
|
|
2064
|
-
assert.deepEqual(body.thinking, { type: 'adaptive', display: 'summarized' });
|
|
2065
|
-
});
|
|
2066
|
-
|
|
2067
|
-
test('anthropic effort: explicit thinkingBudgetTokens wins over effort', () => {
|
|
2068
|
-
const model = 'claude-sonnet-4-6';
|
|
2069
|
-
const body = _buildRequestBodyForCacheSmoke(
|
|
2070
|
-
[{ role: 'user', content: 'hi' }],
|
|
2071
|
-
model,
|
|
2072
|
-
[],
|
|
2073
|
-
{ effort: 'low', thinkingBudgetTokens: 2048 },
|
|
2074
|
-
);
|
|
2075
|
-
assert.deepEqual(body.thinking, { type: 'enabled', budget_tokens: 2048 });
|
|
2076
|
-
assert.equal(body.output_config, undefined);
|
|
2077
|
-
assert.equal(shouldIncludeEffortBeta(model, { effort: 'low', thinkingBudgetTokens: 2048 }), false);
|
|
2078
|
-
});
|
|
2079
|
-
// === 9. OpenAI OAuth WS cache tracing ======================================
|
|
2080
|
-
|
|
2081
|
-
test('openai oauth ws delta: default delta safely falls back on request-property mismatch', () => {
|
|
2082
|
-
const body = { model: 'gpt-5.5', input: [{ type: 'message', role: 'user', content: 'hi' }] };
|
|
2083
|
-
const delta = _computeDelta({
|
|
2084
|
-
entry: {
|
|
2085
|
-
lastRequestSansInput: '{}',
|
|
2086
|
-
lastResponseId: 'resp_prev',
|
|
2087
|
-
lastRequestInput: body.input,
|
|
2088
|
-
lastResponseItems: [],
|
|
2089
|
-
},
|
|
2090
|
-
body,
|
|
2091
|
-
});
|
|
2092
|
-
assert.equal(delta.mode, 'full');
|
|
2093
|
-
assert.equal(delta.reason, 'request_properties_changed');
|
|
2094
|
-
assert.equal(delta.frame.previous_response_id, undefined);
|
|
2095
|
-
});
|
|
2096
|
-
|
|
2097
|
-
test('openai oauth ws delta: ws-delta mode uses previous_response_id without turn-state, keeps safe fallback', () => {
|
|
2098
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2099
|
-
try {
|
|
2100
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2101
|
-
const body = {
|
|
2102
|
-
model: 'gpt-5.5',
|
|
2103
|
-
input: [
|
|
2104
|
-
{ type: 'message', role: 'user', content: 'prev' },
|
|
2105
|
-
{ type: 'message', role: 'user', content: 'next' },
|
|
2106
|
-
],
|
|
2107
|
-
};
|
|
2108
|
-
const entry = {
|
|
2109
|
-
lastRequestSansInput: '{"model":"gpt-5.5"}',
|
|
2110
|
-
lastResponseId: 'resp_prev',
|
|
2111
|
-
lastRequestInput: [body.input[0]],
|
|
2112
|
-
lastResponseItems: [],
|
|
2113
|
-
// NOTE: no turnState — ws-delta (refs) mode must still emit a delta.
|
|
2114
|
-
};
|
|
2115
|
-
|
|
2116
|
-
const refs = _computeDelta({ entry, body });
|
|
2117
|
-
assert.equal(refs.mode, 'delta');
|
|
2118
|
-
assert.equal(refs.frame.previous_response_id, 'resp_prev');
|
|
2119
|
-
assert.deepEqual(refs.frame.input, [body.input[1]]);
|
|
2120
|
-
|
|
2121
|
-
// Safe fallback preserved: a changed request property breaks the prefix
|
|
2122
|
-
// and retreats to a full frame even in ws-delta mode.
|
|
2123
|
-
const changed = _computeDelta({ entry, body: { ...body, model: 'gpt-5.6' } });
|
|
2124
|
-
assert.equal(changed.mode, 'full');
|
|
2125
|
-
assert.equal(changed.reason, 'request_properties_changed');
|
|
2126
|
-
assert.equal(changed.frame.previous_response_id, undefined);
|
|
2127
|
-
|
|
2128
|
-
const noAnchor = _computeDelta({ entry: { ...entry, lastResponseId: null }, body });
|
|
2129
|
-
assert.equal(noAnchor.mode, 'full');
|
|
2130
|
-
assert.equal(noAnchor.reason, 'no_anchor');
|
|
2131
|
-
assert.equal(noAnchor.frame.previous_response_id, undefined);
|
|
2132
|
-
|
|
2133
|
-
const prefixMismatch = _computeDelta({
|
|
2134
|
-
entry: { ...entry, lastRequestInput: [{ type: 'message', role: 'user', content: 'other' }] },
|
|
2135
|
-
body,
|
|
2136
|
-
});
|
|
2137
|
-
assert.equal(prefixMismatch.mode, 'full');
|
|
2138
|
-
assert.equal(prefixMismatch.reason, 'input_prefix_mismatch');
|
|
2139
|
-
assert.equal(prefixMismatch.frame.previous_response_id, undefined);
|
|
2140
|
-
|
|
2141
|
-
const responseMismatch = _computeDelta({
|
|
2142
|
-
entry: {
|
|
2143
|
-
...entry,
|
|
2144
|
-
lastRequestInput: [body.input[0]],
|
|
2145
|
-
lastResponseItems: [{
|
|
2146
|
-
type: 'function_call',
|
|
2147
|
-
call_id: 'call_1',
|
|
2148
|
-
name: 'tool',
|
|
2149
|
-
arguments: '{"api_key":"provider-function-secret","nested":{"z":2,"a":1}}',
|
|
2150
|
-
}],
|
|
2151
|
-
},
|
|
2152
|
-
body: {
|
|
2153
|
-
...body,
|
|
2154
|
-
input: [
|
|
2155
|
-
body.input[0],
|
|
2156
|
-
{
|
|
2157
|
-
type: 'function_call',
|
|
2158
|
-
call_id: 'call_other',
|
|
2159
|
-
name: 'tool',
|
|
2160
|
-
arguments: '{"nested":{"a":1,"z":2},"api_key":"replayed-function-secret"}',
|
|
2161
|
-
},
|
|
2162
|
-
body.input[1],
|
|
2163
|
-
],
|
|
2164
|
-
},
|
|
2165
|
-
});
|
|
2166
|
-
assert.equal(responseMismatch.mode, 'full');
|
|
2167
|
-
assert.equal(responseMismatch.reason, 'response_output_mismatch:function_call');
|
|
2168
|
-
assert.equal(responseMismatch.frame.previous_response_id, undefined);
|
|
2169
|
-
const mismatch = responseMismatch.responseOutputMismatch;
|
|
2170
|
-
assert.deepEqual({
|
|
2171
|
-
expectedType: mismatch.response_output_mismatch_expected_type,
|
|
2172
|
-
actualType: mismatch.response_output_mismatch_actual_type,
|
|
2173
|
-
expectedCount: mismatch.response_output_mismatch_expected_response_item_count,
|
|
2174
|
-
actualCount: mismatch.response_output_mismatch_actual_replayed_input_item_count,
|
|
2175
|
-
}, {
|
|
2176
|
-
expectedType: 'function_call',
|
|
2177
|
-
actualType: 'function_call',
|
|
2178
|
-
expectedCount: 1,
|
|
2179
|
-
actualCount: 2,
|
|
2180
|
-
});
|
|
2181
|
-
assert.match(mismatch.response_output_mismatch_expected_hash, /^[a-f0-9]{64}$/);
|
|
2182
|
-
assert.match(mismatch.response_output_mismatch_actual_hash, /^[a-f0-9]{64}$/);
|
|
2183
|
-
const traceSafe = JSON.stringify(mismatch);
|
|
2184
|
-
assert.equal(traceSafe.includes('call_other'), false);
|
|
2185
|
-
assert.equal(traceSafe.includes('provider-function-secret'), false);
|
|
2186
|
-
assert.equal(traceSafe.includes('replayed-function-secret'), false);
|
|
2187
|
-
} finally {
|
|
2188
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2189
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2190
|
-
}
|
|
2191
|
-
});
|
|
2192
|
-
|
|
2193
|
-
test('openai oauth ws delta: message mismatch diagnostics hash normalized content without tracing it', () => {
|
|
2194
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2195
|
-
try {
|
|
2196
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2197
|
-
const prior = { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'prior' }] };
|
|
2198
|
-
const expected = { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'provider secret' }] };
|
|
2199
|
-
const actual = { type: 'message', role: 'assistant', content: [{ type: 'input_text', text: 'replayed secret' }] };
|
|
2200
|
-
const delta = _computeDelta({
|
|
2201
|
-
entry: {
|
|
2202
|
-
lastRequestSansInput: '{"model":"gpt-5.5"}',
|
|
2203
|
-
lastResponseId: 'resp_prev',
|
|
2204
|
-
lastRequestInput: [prior],
|
|
2205
|
-
lastResponseItems: [expected],
|
|
2206
|
-
},
|
|
2207
|
-
body: { model: 'gpt-5.5', input: [prior, actual] },
|
|
2208
|
-
});
|
|
2209
|
-
const mismatch = delta.responseOutputMismatch;
|
|
2210
|
-
assert.equal(delta.reason, 'response_output_mismatch:message');
|
|
2211
|
-
assert.equal(mismatch.response_output_mismatch_expected_type, 'message');
|
|
2212
|
-
assert.equal(mismatch.response_output_mismatch_actual_type, 'message');
|
|
2213
|
-
assert.notEqual(mismatch.response_output_mismatch_expected_hash, mismatch.response_output_mismatch_actual_hash);
|
|
2214
|
-
const traceSafe = JSON.stringify(mismatch);
|
|
2215
|
-
assert.equal(traceSafe.includes('provider secret'), false);
|
|
2216
|
-
assert.equal(traceSafe.includes('replayed secret'), false);
|
|
2217
|
-
} finally {
|
|
2218
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2219
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2220
|
-
}
|
|
2221
|
-
});
|
|
2222
|
-
|
|
2223
|
-
test('openai oauth ws delta: diagnostic hashes normalize equivalent message and function-call forms', () => {
|
|
2224
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2225
|
-
try {
|
|
2226
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2227
|
-
const prior = { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'prior' }] };
|
|
2228
|
-
const mismatch = (expected, actual) => _computeDelta({
|
|
2229
|
-
entry: {
|
|
2230
|
-
lastRequestSansInput: '{"model":"gpt-5.5"}',
|
|
2231
|
-
lastResponseId: 'resp_prev',
|
|
2232
|
-
lastRequestInput: [prior],
|
|
2233
|
-
lastResponseItems: [expected],
|
|
2234
|
-
},
|
|
2235
|
-
body: { model: 'gpt-5.5', input: [prior, actual] },
|
|
2236
|
-
}).responseOutputMismatch;
|
|
2237
|
-
const message = { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'same sensitive message' }] };
|
|
2238
|
-
const messageExpected = mismatch(message, {
|
|
2239
|
-
type: 'message',
|
|
2240
|
-
role: 'assistant',
|
|
2241
|
-
content: [{ type: 'output_text', text: 'different message' }],
|
|
2242
|
-
});
|
|
2243
|
-
const messageActual = mismatch({
|
|
2244
|
-
type: 'message',
|
|
2245
|
-
role: 'assistant',
|
|
2246
|
-
content: [{ type: 'output_text', text: 'different message' }],
|
|
2247
|
-
}, {
|
|
2248
|
-
type: 'message',
|
|
2249
|
-
role: 'assistant',
|
|
2250
|
-
content: [{ type: 'input_text', text: 'same sensitive message' }],
|
|
2251
|
-
});
|
|
2252
|
-
assert.equal(
|
|
2253
|
-
messageExpected.response_output_mismatch_expected_hash,
|
|
2254
|
-
messageActual.response_output_mismatch_actual_hash,
|
|
2255
|
-
);
|
|
2256
|
-
|
|
2257
|
-
const functionExpected = (argumentsValue) => mismatch({
|
|
2258
|
-
type: 'function_call',
|
|
2259
|
-
call_id: 'call_same',
|
|
2260
|
-
name: 'tool',
|
|
2261
|
-
arguments: argumentsValue,
|
|
2262
|
-
}, {
|
|
2263
|
-
type: 'function_call',
|
|
2264
|
-
call_id: 'call_different',
|
|
2265
|
-
name: 'tool',
|
|
2266
|
-
arguments: '{"ignored":"replayed-function-secret"}',
|
|
2267
|
-
});
|
|
2268
|
-
assert.equal(
|
|
2269
|
-
functionExpected('{"api_key":"function-secret","nested":{"z":2,"a":1}}').response_output_mismatch_expected_hash,
|
|
2270
|
-
functionExpected('{"nested":{"a":1,"z":2},"api_key":"function-secret"}').response_output_mismatch_expected_hash,
|
|
2271
|
-
);
|
|
2272
|
-
} finally {
|
|
2273
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2274
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2275
|
-
}
|
|
2276
|
-
});
|
|
2277
|
-
|
|
2278
|
-
test('openai oauth ws delta: mismatch diagnostics reach transport and cache_break without sensitive values', async () => {
|
|
2279
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2280
|
-
try {
|
|
2281
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2282
|
-
const rows = [];
|
|
2283
|
-
const prior = { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'prior' }] };
|
|
2284
|
-
const cases = [{
|
|
2285
|
-
expected: {
|
|
2286
|
-
type: 'function_call',
|
|
2287
|
-
call_id: 'call_expected',
|
|
2288
|
-
name: 'tool',
|
|
2289
|
-
arguments: '{"api_key":"provider-function-secret","nested":{"z":2,"a":1}}',
|
|
2290
|
-
},
|
|
2291
|
-
actual: {
|
|
2292
|
-
type: 'function_call',
|
|
2293
|
-
call_id: 'call_actual',
|
|
2294
|
-
name: 'tool',
|
|
2295
|
-
arguments: '{"nested":{"a":1,"z":2},"api_key":"replayed-function-secret"}',
|
|
2296
|
-
},
|
|
2297
|
-
}, {
|
|
2298
|
-
expected: {
|
|
2299
|
-
type: 'custom_tool_call',
|
|
2300
|
-
call_id: 'custom_expected',
|
|
2301
|
-
name: 'apply_patch',
|
|
2302
|
-
input: 'provider-custom-input-secret',
|
|
2303
|
-
},
|
|
2304
|
-
actual: {
|
|
2305
|
-
type: 'custom_tool_call',
|
|
2306
|
-
call_id: 'custom_actual',
|
|
2307
|
-
name: 'apply_patch',
|
|
2308
|
-
input: 'replayed-custom-input-secret',
|
|
2309
|
-
},
|
|
2310
|
-
}];
|
|
2311
|
-
for (const { expected, actual } of cases) {
|
|
2312
|
-
const body = { model: 'gpt-5.5', input: [prior, actual] };
|
|
2313
|
-
const entry = {
|
|
2314
|
-
socket: { close() {} },
|
|
2315
|
-
lastRequestSansInput: _stableStringify(_sansInput(body)),
|
|
2316
|
-
lastResponseId: 'resp_prev',
|
|
2317
|
-
lastRequestInput: [prior],
|
|
2318
|
-
lastResponseItems: [expected],
|
|
2319
|
-
};
|
|
2320
|
-
const expectedDiagnostics = _computeDelta({ entry, body }).responseOutputMismatch;
|
|
2321
|
-
await sendViaWebSocket({
|
|
2322
|
-
auth: { type: 'xai', access_token: 'test-token' },
|
|
2323
|
-
body,
|
|
2324
|
-
poolKey: `mismatch-trace-${expected.type}`,
|
|
2325
|
-
cacheKey: 'mismatch-trace-test',
|
|
2326
|
-
iteration: 1,
|
|
2327
|
-
useModel: 'gpt-5.5',
|
|
2328
|
-
traceProvider: 'xai',
|
|
2329
|
-
_acquireWithRetryFn: async () => ({ entry, reused: false }),
|
|
2330
|
-
_sendFrameFn: async () => {},
|
|
2331
|
-
_streamFn: async () => ({
|
|
2332
|
-
content: 'ok',
|
|
2333
|
-
model: 'gpt-5.5',
|
|
2334
|
-
toolCalls: [],
|
|
2335
|
-
usage: {},
|
|
2336
|
-
responseId: 'resp_next',
|
|
2337
|
-
responseItems: [],
|
|
2338
|
-
closeSocket: true,
|
|
2339
|
-
}),
|
|
2340
|
-
_agentTraceFn: (row) => rows.push(row),
|
|
2341
|
-
});
|
|
2342
|
-
const emitted = rows.filter((row) => row.payload?.response_output_mismatch_expected_type === expected.type);
|
|
2343
|
-
assert.deepEqual(emitted.map((row) => row.kind).sort(), ['cache_break', 'transport']);
|
|
2344
|
-
for (const row of emitted) assert.deepEqual(
|
|
2345
|
-
Object.fromEntries(Object.keys(expectedDiagnostics).map((key) => [key, row.payload[key]])),
|
|
2346
|
-
expectedDiagnostics,
|
|
2347
|
-
);
|
|
2348
|
-
}
|
|
2349
|
-
const traceText = JSON.stringify(rows);
|
|
2350
|
-
for (const secret of [
|
|
2351
|
-
'provider-function-secret',
|
|
2352
|
-
'replayed-function-secret',
|
|
2353
|
-
'provider-custom-input-secret',
|
|
2354
|
-
'replayed-custom-input-secret',
|
|
2355
|
-
]) assert.equal(traceText.includes(secret), false);
|
|
2356
|
-
} finally {
|
|
2357
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2358
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2359
|
-
}
|
|
2360
|
-
});
|
|
2361
|
-
|
|
2362
|
-
test('openai oauth ws delta: warmup generate:false does not force request_properties_changed', () => {
|
|
2363
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2364
|
-
try {
|
|
2365
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2366
|
-
const warmupBody = { model: 'gpt-5.5', generate: false, input: [] };
|
|
2367
|
-
const body = { model: 'gpt-5.5', input: [{ type: 'message', role: 'user', content: 'first real input' }] };
|
|
2368
|
-
const entry = {
|
|
2369
|
-
lastRequestSansInput: _stableStringify(_sansInput(warmupBody, { normalizeWarmupGenerate: true })),
|
|
2370
|
-
lastResponseId: 'resp_warm',
|
|
2371
|
-
lastRequestInput: [],
|
|
2372
|
-
lastResponseItems: [],
|
|
2373
|
-
};
|
|
2374
|
-
assert.equal(_sansInput(warmupBody, { normalizeWarmupGenerate: true }).generate, undefined);
|
|
2375
|
-
const delta = _computeDelta({ entry, body, traceProvider: 'openai-oauth' });
|
|
2376
|
-
assert.equal(delta.mode, 'delta');
|
|
2377
|
-
assert.equal(delta.frame.previous_response_id, 'resp_warm');
|
|
2378
|
-
assert.deepEqual(delta.frame.input, body.input);
|
|
2379
|
-
// Non-warmup generate difference still breaks the delta.
|
|
2380
|
-
const genEntry = {
|
|
2381
|
-
...entry,
|
|
2382
|
-
lastRequestSansInput: _stableStringify(_sansInput(
|
|
2383
|
-
{ ...warmupBody, generate: true },
|
|
2384
|
-
{ normalizeWarmupGenerate: true },
|
|
2385
|
-
)),
|
|
2386
|
-
};
|
|
2387
|
-
const genChanged = _computeDelta({ entry: genEntry, body, traceProvider: 'openai-oauth' });
|
|
2388
|
-
assert.equal(genChanged.mode, 'full');
|
|
2389
|
-
assert.equal(genChanged.reason, 'request_properties_changed');
|
|
2390
|
-
} finally {
|
|
2391
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2392
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2393
|
-
}
|
|
2394
|
-
});
|
|
2395
|
-
|
|
2396
|
-
test('xAI keeps generate:false in the property guard and falls back to a full frame', () => {
|
|
2397
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2398
|
-
try {
|
|
2399
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2400
|
-
const warmupBody = { model: 'grok-4', generate: false, input: [] };
|
|
2401
|
-
const body = { model: 'grok-4', input: [{ role: 'user', content: 'real request' }] };
|
|
2402
|
-
const entry = {
|
|
2403
|
-
lastRequestSansInput: _stableStringify(_sansInput(warmupBody)),
|
|
2404
|
-
lastResponseId: 'xai-warm',
|
|
2405
|
-
lastRequestInput: [],
|
|
2406
|
-
lastResponseItems: [],
|
|
2407
|
-
};
|
|
2408
|
-
assert.equal(_sansInput(warmupBody).generate, false);
|
|
2409
|
-
const delta = _computeDelta({ entry, body, traceProvider: 'xai' });
|
|
2410
|
-
assert.equal(delta.mode, 'full');
|
|
2411
|
-
assert.equal(delta.reason, 'request_properties_changed');
|
|
2412
|
-
assert.equal(delta.frame.previous_response_id, undefined);
|
|
2413
|
-
assert.deepEqual(delta.frame.input, body.input);
|
|
2414
|
-
} finally {
|
|
2415
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2416
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2417
|
-
}
|
|
2418
|
-
});
|
|
2419
|
-
|
|
2420
|
-
test('OpenAI OAuth startup prewarm sends no transcript and anchors the first real WS request', async () => {
|
|
2421
|
-
const savedEnv = Object.fromEntries([
|
|
2422
|
-
'MIXDOG_OAI_TRANSPORT',
|
|
2423
|
-
'MIXDOG_OPENAI_OAUTH_WS_WARMUP',
|
|
2424
|
-
'MIXDOG_AGENT_TRACE_DISABLE',
|
|
2425
|
-
].map((name) => [name, process.env[name]]));
|
|
2426
|
-
Object.assign(process.env, {
|
|
2427
|
-
MIXDOG_OAI_TRANSPORT: 'ws-delta',
|
|
2428
|
-
MIXDOG_OPENAI_OAUTH_WS_WARMUP: '1',
|
|
2429
|
-
MIXDOG_AGENT_TRACE_DISABLE: '1',
|
|
2430
|
-
});
|
|
2431
|
-
try {
|
|
2432
|
-
const provider = new OpenAIOAuthProvider({});
|
|
2433
|
-
provider.ensureAuth = async () => ({ access_token: 'test-token' });
|
|
2434
|
-
const liveInput = [{
|
|
2435
|
-
type: 'message',
|
|
2436
|
-
role: 'user',
|
|
2437
|
-
content: [{ type: 'input_text', text: 'live-transcript-must-appear-once' }],
|
|
2438
|
-
}];
|
|
2439
|
-
const body = {
|
|
2440
|
-
model: 'gpt-5.5',
|
|
2441
|
-
instructions: 'base instructions',
|
|
2442
|
-
input: liveInput,
|
|
2443
|
-
tools: [{ type: 'function', name: 'read', parameters: { type: 'object' } }],
|
|
2444
|
-
stream: true,
|
|
2445
|
-
background: false,
|
|
2446
|
-
prompt_cache_key: 'prewarm-parity-test',
|
|
2447
|
-
};
|
|
2448
|
-
const frames = [];
|
|
2449
|
-
let streams = 0;
|
|
2450
|
-
const result = await provider.send([], 'gpt-5.5', [], {
|
|
2451
|
-
sessionId: 'prewarm-parity-test',
|
|
2452
|
-
_prebuiltBody: body,
|
|
2453
|
-
_sendViaWebSocketFn: (args) => sendViaWebSocket({
|
|
2454
|
-
...args,
|
|
2455
|
-
_acquireWithRetryFn: async () => ({ entry: { socket: { close() {} } }, reused: false }),
|
|
2456
|
-
_sendFrameFn: async (_entry, frame) => frames.push(JSON.parse(JSON.stringify(frame))),
|
|
2457
|
-
_streamFn: async ({ state }) => {
|
|
2458
|
-
streams += 1;
|
|
2459
|
-
return state.warmup
|
|
2460
|
-
? {
|
|
2461
|
-
content: '',
|
|
2462
|
-
model: 'gpt-5.5',
|
|
2463
|
-
toolCalls: [],
|
|
2464
|
-
usage: { inputTokens: 10, outputTokens: 0, cachedTokens: 0, promptTokens: 10 },
|
|
2465
|
-
responseId: 'warm-1',
|
|
2466
|
-
responseItems: [],
|
|
2467
|
-
}
|
|
2468
|
-
: {
|
|
2469
|
-
content: 'done',
|
|
2470
|
-
model: 'gpt-5.5',
|
|
2471
|
-
toolCalls: [],
|
|
2472
|
-
usage: { inputTokens: 20, outputTokens: 1, cachedTokens: 10, promptTokens: 20 },
|
|
2473
|
-
responseId: 'resp-1',
|
|
2474
|
-
responseItems: [],
|
|
2475
|
-
closeSocket: true,
|
|
2476
|
-
};
|
|
2477
|
-
},
|
|
2478
|
-
_agentTraceFn: () => {},
|
|
2479
|
-
_sendSpanTraceFn: () => {},
|
|
2480
|
-
}),
|
|
2481
|
-
});
|
|
2482
|
-
|
|
2483
|
-
assert.equal(streams, 2);
|
|
2484
|
-
assert.equal(frames.length, 2);
|
|
2485
|
-
const [warmup, followUp] = frames;
|
|
2486
|
-
assert.equal(warmup.type, 'response.create');
|
|
2487
|
-
assert.equal(warmup.generate, false);
|
|
2488
|
-
assert.deepEqual(warmup.input, []);
|
|
2489
|
-
assert.equal(warmup.instructions, body.instructions);
|
|
2490
|
-
assert.deepEqual(warmup.tools, body.tools);
|
|
2491
|
-
assert.equal(warmup.stream, undefined);
|
|
2492
|
-
assert.equal(warmup.background, undefined);
|
|
2493
|
-
assert.equal(JSON.stringify(warmup).includes('live-transcript-must-appear-once'), false);
|
|
2494
|
-
assert.equal(followUp.type, 'response.create');
|
|
2495
|
-
assert.equal(followUp.previous_response_id, 'warm-1');
|
|
2496
|
-
assert.deepEqual(followUp.input, liveInput);
|
|
2497
|
-
assert.equal(JSON.stringify(followUp).match(/live-transcript-must-appear-once/g)?.length, 1);
|
|
2498
|
-
assert.equal(result.usage.inputTokens, 30, 'warmup remains included in billable totals');
|
|
2499
|
-
assert.equal(result.usage.mainInputTokens, 20, 'main context snapshot remains separate');
|
|
2500
|
-
} finally {
|
|
2501
|
-
for (const [name, value] of Object.entries(savedEnv)) {
|
|
2502
|
-
if (value == null) delete process.env[name];
|
|
2503
|
-
else process.env[name] = value;
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
});
|
|
2507
|
-
|
|
2508
|
-
test('openai oauth ws delta: native tool_search output replays with canonical fields', () => {
|
|
2509
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2510
|
-
try {
|
|
2511
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2512
|
-
const baseTools = [{
|
|
2513
|
-
type: 'tool_search',
|
|
2514
|
-
execution: 'client',
|
|
2515
|
-
description: 'load tools',
|
|
2516
|
-
parameters: { type: 'object', properties: {} },
|
|
2517
|
-
}];
|
|
2518
|
-
const output = {
|
|
2519
|
-
type: 'tool_search_output',
|
|
2520
|
-
call_id: 'call_load_1',
|
|
2521
|
-
status: 'completed',
|
|
2522
|
-
execution: 'client',
|
|
2523
|
-
tools: [{ type: 'function', name: 'read', parameters: { type: 'object', properties: {} } }],
|
|
2524
|
-
};
|
|
2525
|
-
const body = {
|
|
2526
|
-
model: 'gpt-5.5',
|
|
2527
|
-
instructions: 'sys',
|
|
2528
|
-
tools: baseTools,
|
|
2529
|
-
prompt_cache_key: 'cache-key',
|
|
2530
|
-
input: [
|
|
2531
|
-
{ type: 'message', role: 'user', content: 'load read' },
|
|
2532
|
-
{ type: 'tool_search_call', call_id: 'call_load_1', execution: 'client', arguments: { names: ['read'] } },
|
|
2533
|
-
output,
|
|
2534
|
-
{ type: 'message', role: 'user', content: 'use read' },
|
|
2535
|
-
],
|
|
2536
|
-
};
|
|
2537
|
-
const delta = _computeDelta({
|
|
2538
|
-
entry: {
|
|
2539
|
-
lastRequestSansInput: _stableStringify(_sansInput(body)),
|
|
2540
|
-
lastResponseId: 'resp_prev',
|
|
2541
|
-
lastRequestInput: [body.input[0]],
|
|
2542
|
-
lastResponseItems: [body.input[1]],
|
|
2543
|
-
},
|
|
2544
|
-
body,
|
|
2545
|
-
});
|
|
2546
|
-
assert.equal(delta.mode, 'delta');
|
|
2547
|
-
assert.equal(delta.frame.prompt_cache_key, 'cache-key');
|
|
2548
|
-
assert.deepEqual(delta.frame.tools, baseTools);
|
|
2549
|
-
assert.equal(delta.frame.instructions, 'sys');
|
|
2550
|
-
assert.deepEqual(delta.frame.input, [output, body.input[3]]);
|
|
2551
|
-
} finally {
|
|
2552
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2553
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2554
|
-
}
|
|
2555
|
-
});
|
|
2556
|
-
|
|
2557
|
-
test('canonical frame: full-frame builder leads with type and preserves codex body key order', () => {
|
|
2558
|
-
const body = {
|
|
2559
|
-
model: 'gpt-5.5',
|
|
2560
|
-
instructions: 'sys',
|
|
2561
|
-
input: [{ type: 'message', role: 'user', content: 'hi' }],
|
|
2562
|
-
tool_choice: 'auto',
|
|
2563
|
-
parallel_tool_calls: true,
|
|
2564
|
-
reasoning: { effort: 'medium' },
|
|
2565
|
-
store: false,
|
|
2566
|
-
stream: true,
|
|
2567
|
-
include: ['reasoning.encrypted_content'],
|
|
2568
|
-
prompt_cache_key: 'k',
|
|
2569
|
-
text: { verbosity: 'low' },
|
|
2570
|
-
};
|
|
2571
|
-
const frame = _buildResponseCreateFrame(body);
|
|
2572
|
-
assert.deepEqual(Object.keys(frame), ['type', ...Object.keys(body)]);
|
|
2573
|
-
assert.equal(frame.type, 'response.create');
|
|
2574
|
-
// A full-frame build is byte-identical to the legacy spread form.
|
|
2575
|
-
assert.equal(JSON.stringify(frame), JSON.stringify({ type: 'response.create', ...body }));
|
|
2576
|
-
assert.equal(frame.previous_response_id, undefined);
|
|
2577
|
-
});
|
|
2578
|
-
|
|
2579
|
-
test('canonical frame: delta insert keeps key order and chained instructions, overrides input', () => {
|
|
2580
|
-
const body = {
|
|
2581
|
-
model: 'gpt-5.5',
|
|
2582
|
-
instructions: 'sys',
|
|
2583
|
-
input: [{ a: 1 }, { b: 2 }],
|
|
2584
|
-
tool_choice: 'auto',
|
|
2585
|
-
text: { verbosity: 'low' },
|
|
2586
|
-
};
|
|
2587
|
-
const delta = _buildResponseCreateFrame(body, { previousResponseId: 'resp_prev', inputOverride: [{ b: 2 }] });
|
|
2588
|
-
assert.deepEqual(Object.keys(delta), ['type', 'model', 'instructions', 'previous_response_id', 'input', 'tool_choice', 'text']);
|
|
2589
|
-
assert.equal(delta.instructions, 'sys');
|
|
2590
|
-
assert.equal(delta.previous_response_id, 'resp_prev');
|
|
2591
|
-
assert.deepEqual(delta.input, [{ b: 2 }]);
|
|
2592
|
-
// Empty instructions is also dropped in delta mode (server resolves via prev id).
|
|
2593
|
-
const noInstr = _buildResponseCreateFrame({ ...body, instructions: '' }, { previousResponseId: 'resp_prev', inputOverride: [] });
|
|
2594
|
-
assert.deepEqual(Object.keys(noInstr), ['type', 'model', 'previous_response_id', 'input', 'tool_choice', 'text']);
|
|
2595
|
-
});
|
|
2596
|
-
|
|
2597
|
-
test('openai oauth ws cache observation detects warm zero and partial retreats', () => {
|
|
2598
|
-
const zero = _cacheObservationForTest({
|
|
2599
|
-
entry: { promptCacheMaxCachedTokens: 47_616 },
|
|
2600
|
-
result: { usage: { inputTokens: 59_000, promptTokens: 59_000, cachedTokens: 0 } },
|
|
2601
|
-
});
|
|
2602
|
-
assert.equal(zero.actualMiss, true);
|
|
2603
|
-
assert.equal(zero.missReason, 'warm_session_zero_cached_tokens');
|
|
2604
|
-
assert.equal(zero.uncachedTokens, 59_000);
|
|
2605
|
-
|
|
2606
|
-
const partial = _cacheObservationForTest({
|
|
2607
|
-
entry: { promptCacheMaxCachedTokens: 101_888 },
|
|
2608
|
-
result: { usage: { inputTokens: 102_456, promptTokens: 102_456, cachedTokens: 60_928 } },
|
|
2609
|
-
});
|
|
2610
|
-
assert.equal(partial.actualMiss, true);
|
|
2611
|
-
assert.equal(partial.missReason, 'warm_session_cached_tokens_dropped');
|
|
2612
|
-
assert.equal(partial.cachedTokens % 512, 0);
|
|
2613
|
-
|
|
2614
|
-
const healthy = _cacheObservationForTest({
|
|
2615
|
-
entry: { promptCacheMaxCachedTokens: 101_888 },
|
|
2616
|
-
result: { usage: { inputTokens: 106_468, promptTokens: 106_468, cachedTokens: 103_424 } },
|
|
2617
|
-
});
|
|
2618
|
-
assert.equal(healthy.actualMiss, false);
|
|
2619
|
-
assert.equal(healthy.missReason, null);
|
|
2620
|
-
|
|
2621
|
-
const compacted = _cacheObservationForTest({
|
|
2622
|
-
entry: { promptCacheMaxCachedTokens: 255_488 },
|
|
2623
|
-
result: { usage: { inputTokens: 21_066, promptTokens: 21_066, cachedTokens: 8_704 } },
|
|
2624
|
-
continuityResetReason: 'input_prefix_mismatch',
|
|
2625
|
-
});
|
|
2626
|
-
assert.equal(compacted.actualMiss, false, 'intentional prompt rewrite must reset the old high-water');
|
|
2627
|
-
assert.equal(compacted.wasWarm, false);
|
|
2628
|
-
|
|
2629
|
-
const previousInput = [{ type: 'message', role: 'user', content: 'old long transcript' }];
|
|
2630
|
-
const rewrittenBody = { model: 'gpt-5.5', input: [{ type: 'message', role: 'user', content: 'compact summary' }] };
|
|
2631
|
-
assert.equal(_cacheContinuityResetReasonForTest({
|
|
2632
|
-
mode: 'full',
|
|
2633
|
-
deltaReason: 'full_default',
|
|
2634
|
-
entry: {
|
|
2635
|
-
lastResponseId: 'resp_old',
|
|
2636
|
-
lastRequestSansInput: _stableStringify(_sansInput(rewrittenBody)),
|
|
2637
|
-
lastRequestInput: previousInput,
|
|
2638
|
-
},
|
|
2639
|
-
body: rewrittenBody,
|
|
2640
|
-
}), 'input_prefix_mismatch', 'ws-full must detect prompt rewrites hidden by full_default');
|
|
2641
|
-
});
|
|
2642
|
-
|
|
2643
|
-
// === 10. OpenAI transport-policy switch (MIXDOG_OAI_TRANSPORT) ==============
|
|
2644
|
-
// One clean knob selects among ws-full | ws-delta | http-sse | auto. The
|
|
2645
|
-
// resolver is a pure function over an injected env, and the delta gate
|
|
2646
|
-
// (_computeDelta) + transport dispatch both read it, so these unit tests pin
|
|
2647
|
-
// the resolution and the delta branching without any network.
|
|
2648
|
-
import {
|
|
2649
|
-
resolveOpenAiTransportPolicy,
|
|
2650
|
-
_normalizeTransportMode,
|
|
2651
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs';
|
|
2652
|
-
import {
|
|
2653
|
-
resolveResponsesTransportPolicy,
|
|
2654
|
-
RESPONSES_TRANSPORT_CAPABILITIES,
|
|
2655
|
-
_gateTransportMode,
|
|
2656
|
-
FULL_RESPONSES_TRANSPORT_CAPS,
|
|
2657
|
-
} from '../src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs';
|
|
2658
|
-
|
|
2659
|
-
test('transport policy: default (no env) is auto WS-first / refs continuation ON / HTTP fallback ON', () => {
|
|
2660
|
-
const p = resolveOpenAiTransportPolicy({});
|
|
2661
|
-
assert.equal(p.mode, 'auto');
|
|
2662
|
-
assert.equal(p.transport, 'ws');
|
|
2663
|
-
assert.equal(p.allowHttpFallback, true);
|
|
2664
|
-
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
2665
|
-
});
|
|
2666
|
-
|
|
2667
|
-
test('transport policy: default ignores the legacy MIXDOG_OAI_WS_DELTA env', () => {
|
|
2668
|
-
// Legacy compatibility removed: delta is selected solely via ws-delta mode.
|
|
2669
|
-
assert.deepEqual(resolveOpenAiTransportPolicy({ MIXDOG_OAI_WS_DELTA: '1' }).delta, { force: false, refs: true, optIn: true });
|
|
2670
|
-
assert.deepEqual(resolveOpenAiTransportPolicy({ MIXDOG_OAI_WS_DELTA: 'force' }).delta, { force: false, refs: true, optIn: true });
|
|
2671
|
-
assert.deepEqual(resolveOpenAiTransportPolicy({ MIXDOG_OAI_WS_DELTA: 'refs' }).delta, { force: false, refs: true, optIn: true });
|
|
2672
|
-
});
|
|
2673
|
-
|
|
2674
|
-
test('transport policy: ws-full forces full frames', () => {
|
|
2675
|
-
const p = resolveOpenAiTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'ws-full' });
|
|
2676
|
-
assert.equal(p.transport, 'ws');
|
|
2677
|
-
assert.equal(p.allowHttpFallback, false);
|
|
2678
|
-
assert.equal(p.delta.optIn, false);
|
|
2679
|
-
});
|
|
2680
|
-
|
|
2681
|
-
test('transport policy: ws-delta selects refs-compatible delta (no turn-state demand)', () => {
|
|
2682
|
-
const p = resolveOpenAiTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'ws-delta' });
|
|
2683
|
-
assert.equal(p.transport, 'ws');
|
|
2684
|
-
assert.equal(p.allowHttpFallback, false);
|
|
2685
|
-
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
2686
|
-
});
|
|
2687
|
-
|
|
2688
|
-
test('transport policy: http-sse forces the HTTP transport with delta OFF', () => {
|
|
2689
|
-
const p = resolveOpenAiTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'http-sse' });
|
|
2690
|
-
assert.equal(p.transport, 'http');
|
|
2691
|
-
assert.equal(p.allowHttpFallback, false);
|
|
2692
|
-
assert.equal(p.delta.optIn, false);
|
|
2693
|
-
});
|
|
2694
|
-
|
|
2695
|
-
test('transport policy: unknown MIXDOG_OAI_TRANSPORT falls back to default auto', () => {
|
|
2696
|
-
const p = resolveOpenAiTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'quantum' });
|
|
2697
|
-
assert.equal(p.mode, 'auto');
|
|
2698
|
-
assert.equal(p.transport, 'ws');
|
|
2699
|
-
assert.equal(p.allowHttpFallback, true);
|
|
2700
|
-
});
|
|
2701
|
-
|
|
2702
|
-
test('response frame builder can omit transport-only stream/background for codex warmup parity', () => {
|
|
2703
|
-
const body = { model: 'gpt-5.5', input: [{ a: 1 }, { b: 2 }], stream: true, background: false, text: { verbosity: 'low' } };
|
|
2704
|
-
const full = _buildResponseCreateFrame(body, { omitTransportFields: true });
|
|
2705
|
-
assert.equal('stream' in full || 'background' in full, false);
|
|
2706
|
-
const d = _buildResponseCreateFrame(body, { previousResponseId: 'resp_prev', inputOverride: [{ b: 2 }], omitTransportFields: true });
|
|
2707
|
-
assert.deepEqual([d.previous_response_id, d.input, 'stream' in d], ['resp_prev', [{ b: 2 }], false]);
|
|
2708
|
-
});
|
|
2709
|
-
|
|
2710
|
-
test('transport policy: mode token aliases normalize', () => {
|
|
2711
|
-
assert.equal(_normalizeTransportMode('WS_FULL'), 'ws-full');
|
|
2712
|
-
assert.equal(_normalizeTransportMode(' ws delta '), 'ws-delta');
|
|
2713
|
-
assert.equal(_normalizeTransportMode('http/sse'), 'http-sse');
|
|
2714
|
-
assert.equal(_normalizeTransportMode('sse'), 'http-sse');
|
|
2715
|
-
assert.equal(_normalizeTransportMode('auto'), 'auto');
|
|
2716
|
-
assert.equal(_normalizeTransportMode('official'), null);
|
|
2717
|
-
assert.equal(_normalizeTransportMode(''), null);
|
|
2718
|
-
assert.equal(_normalizeTransportMode('bogus'), null);
|
|
2719
|
-
});
|
|
2720
|
-
|
|
2721
|
-
test('transport policy: ws-delta drives _computeDelta to emit a delta frame', () => {
|
|
2722
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2723
|
-
try {
|
|
2724
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2725
|
-
const body = {
|
|
2726
|
-
model: 'gpt-5.5',
|
|
2727
|
-
input: [
|
|
2728
|
-
{ type: 'message', role: 'user', content: 'prev' },
|
|
2729
|
-
{ type: 'message', role: 'user', content: 'next' },
|
|
2730
|
-
],
|
|
2731
|
-
};
|
|
2732
|
-
const entry = {
|
|
2733
|
-
lastRequestSansInput: '{"model":"gpt-5.5"}',
|
|
2734
|
-
lastResponseId: 'resp_prev',
|
|
2735
|
-
lastRequestInput: [body.input[0]],
|
|
2736
|
-
lastResponseItems: [],
|
|
2737
|
-
// no turnState — refs mode must still delta
|
|
2738
|
-
};
|
|
2739
|
-
const delta = _computeDelta({ entry, body });
|
|
2740
|
-
assert.equal(delta.mode, 'delta');
|
|
2741
|
-
assert.equal(delta.frame.previous_response_id, 'resp_prev');
|
|
2742
|
-
assert.deepEqual(delta.frame.input, [body.input[1]]);
|
|
2743
|
-
} finally {
|
|
2744
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2745
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2746
|
-
}
|
|
2747
|
-
});
|
|
2748
|
-
|
|
2749
|
-
test('transport policy: ws-full forces _computeDelta to full (delta OFF)', () => {
|
|
2750
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2751
|
-
try {
|
|
2752
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-full';
|
|
2753
|
-
const body = { model: 'gpt-5.5', input: [{ type: 'message', role: 'user', content: 'hi' }] };
|
|
2754
|
-
const delta = _computeDelta({
|
|
2755
|
-
entry: {
|
|
2756
|
-
lastRequestSansInput: '{"model":"gpt-5.5"}',
|
|
2757
|
-
lastResponseId: 'resp_prev',
|
|
2758
|
-
lastRequestInput: body.input,
|
|
2759
|
-
lastResponseItems: [],
|
|
2760
|
-
},
|
|
2761
|
-
body,
|
|
2762
|
-
});
|
|
2763
|
-
assert.equal(delta.mode, 'full');
|
|
2764
|
-
assert.equal(delta.reason, 'full_default');
|
|
2765
|
-
assert.equal(delta.frame.previous_response_id, undefined);
|
|
2766
|
-
} finally {
|
|
2767
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2768
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2769
|
-
}
|
|
2770
|
-
});
|
|
2771
|
-
|
|
2772
|
-
// === 10b. Shared Responses transport policy (capability gating) ============
|
|
2773
|
-
// resolveResponsesTransportPolicy generalizes the OpenAI switch across every
|
|
2774
|
-
// Responses backend. Full-capability providers (OpenAI OAuth/direct) resolve
|
|
2775
|
-
// byte-identically to resolveOpenAiTransportPolicy; xAI/Grok also carry WS
|
|
2776
|
-
// delta capability, so default/ws-delta drive the OFFICIAL xAI continuation
|
|
2777
|
-
// (previous_response_id + incremental input) rather than collapsing to
|
|
2778
|
-
// 'ws-full'.
|
|
2779
|
-
|
|
2780
|
-
test('responses transport policy: full caps === legacy OpenAI resolver (byte-identical)', () => {
|
|
2781
|
-
for (const env of [
|
|
2782
|
-
{},
|
|
2783
|
-
{ MIXDOG_OAI_TRANSPORT: 'ws-delta' },
|
|
2784
|
-
{ MIXDOG_OAI_TRANSPORT: 'ws-full' },
|
|
2785
|
-
{ MIXDOG_OAI_TRANSPORT: 'http-sse' },
|
|
2786
|
-
{ MIXDOG_OAI_TRANSPORT: 'quantum' },
|
|
2787
|
-
]) {
|
|
2788
|
-
const legacy = resolveOpenAiTransportPolicy(env);
|
|
2789
|
-
const shared = resolveResponsesTransportPolicy(env, RESPONSES_TRANSPORT_CAPABILITIES['openai-oauth']);
|
|
2790
|
-
assert.equal(shared.mode, legacy.mode);
|
|
2791
|
-
assert.equal(shared.transport, legacy.transport);
|
|
2792
|
-
assert.equal(shared.allowHttpFallback, legacy.allowHttpFallback);
|
|
2793
|
-
assert.deepEqual(shared.delta, legacy.delta);
|
|
2794
|
-
}
|
|
2795
|
-
});
|
|
2796
|
-
|
|
2797
|
-
test('responses transport policy: openai direct caps match oauth (both full)', () => {
|
|
2798
|
-
const env = { MIXDOG_OAI_TRANSPORT: 'ws-delta' };
|
|
2799
|
-
const direct = resolveResponsesTransportPolicy(env, RESPONSES_TRANSPORT_CAPABILITIES.openai);
|
|
2800
|
-
assert.equal(direct.transport, 'ws');
|
|
2801
|
-
assert.deepEqual(direct.delta, { force: false, refs: true, optIn: true });
|
|
2802
|
-
});
|
|
2803
|
-
|
|
2804
|
-
test('responses transport policy: xai auto is WS-first with HTTP fallback', () => {
|
|
2805
|
-
const p = resolveResponsesTransportPolicy({}, RESPONSES_TRANSPORT_CAPABILITIES.xai);
|
|
2806
|
-
assert.equal(p.mode, 'auto');
|
|
2807
|
-
assert.equal(p.transport, 'ws');
|
|
2808
|
-
assert.equal(p.allowHttpFallback, true);
|
|
2809
|
-
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
2810
|
-
});
|
|
2811
|
-
|
|
2812
|
-
test('responses transport policy: explicit auto is WS-first with HTTP fallback', () => {
|
|
2813
|
-
const p = resolveResponsesTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'auto' }, RESPONSES_TRANSPORT_CAPABILITIES.xai);
|
|
2814
|
-
assert.equal(p.mode, 'auto');
|
|
2815
|
-
assert.equal(p.transport, 'ws');
|
|
2816
|
-
assert.equal(p.allowHttpFallback, true);
|
|
2817
|
-
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
2818
|
-
});
|
|
2819
|
-
|
|
2820
|
-
test('responses transport policy: xai ws-delta drives official continuation (refs delta ON)', () => {
|
|
2821
|
-
const p = resolveResponsesTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'ws-delta' }, RESPONSES_TRANSPORT_CAPABILITIES.xai);
|
|
2822
|
-
assert.equal(p.requestedMode, 'ws-delta');
|
|
2823
|
-
assert.equal(p.mode, 'ws-delta');
|
|
2824
|
-
assert.equal(p.transport, 'ws');
|
|
2825
|
-
assert.equal(p.allowHttpFallback, false);
|
|
2826
|
-
// Official xAI continuation: previous_response_id + incremental input, no
|
|
2827
|
-
// Codex turn-state — refs delta ON.
|
|
2828
|
-
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
2829
|
-
});
|
|
2830
|
-
|
|
2831
|
-
test('responses transport policy: xai ws-full → WS, http-sse → HTTP', () => {
|
|
2832
|
-
const wsFull = resolveResponsesTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'ws-full' }, RESPONSES_TRANSPORT_CAPABILITIES.xai);
|
|
2833
|
-
assert.equal(wsFull.transport, 'ws');
|
|
2834
|
-
assert.equal(wsFull.allowHttpFallback, false);
|
|
2835
|
-
const http = resolveResponsesTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'http-sse' }, RESPONSES_TRANSPORT_CAPABILITIES.xai);
|
|
2836
|
-
assert.equal(http.transport, 'http');
|
|
2837
|
-
assert.equal(http.allowHttpFallback, false);
|
|
2838
|
-
});
|
|
2839
|
-
|
|
2840
|
-
test('openai-compat/xai: HTTP pin uses only the injected preconnect seam', async () => {
|
|
2841
|
-
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2842
|
-
const prevFetch = globalThis.fetch;
|
|
2843
|
-
let outboundFetchAttempts = 0;
|
|
2844
|
-
let injectedPreconnectCalls = 0;
|
|
2845
|
-
try {
|
|
2846
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2847
|
-
globalThis.fetch = async () => {
|
|
2848
|
-
outboundFetchAttempts += 1;
|
|
2849
|
-
throw new Error('provider transport test attempted outbound fetch');
|
|
2850
|
-
};
|
|
2851
|
-
|
|
2852
|
-
// Resolve the actual imported binding and inspect _doSend before
|
|
2853
|
-
// invoking any outbound-capable provider path. AST traversal naturally
|
|
2854
|
-
// excludes comments/strings and catches direct, aliased, parenthesized,
|
|
2855
|
-
// optional, or assigned references.
|
|
2856
|
-
const compatSource = readFileSync(
|
|
2857
|
-
new URL('../src/runtime/agent/orchestrator/providers/openai-compat.mjs', import.meta.url),
|
|
2858
|
-
'utf8',
|
|
2859
|
-
);
|
|
2860
|
-
const compatAst = parse(compatSource, {
|
|
2861
|
-
ecmaVersion: 'latest',
|
|
2862
|
-
sourceType: 'module',
|
|
2863
|
-
locations: true,
|
|
2864
|
-
ranges: true,
|
|
2865
|
-
});
|
|
2866
|
-
const preconnectImport = compatAst.body
|
|
2867
|
-
.filter(node => node.type === 'ImportDeclaration')
|
|
2868
|
-
.flatMap(node => node.specifiers)
|
|
2869
|
-
.find(specifier =>
|
|
2870
|
-
specifier.type === 'ImportSpecifier'
|
|
2871
|
-
&& specifier.imported.name === 'preconnect',
|
|
2872
|
-
);
|
|
2873
|
-
assert.ok(preconnectImport, 'shared preconnect import binding must be resolvable');
|
|
2874
|
-
const scopeManager = analyze(compatAst, { ecmaVersion: 2022, sourceType: 'module' });
|
|
2875
|
-
const moduleScope = scopeManager.scopes.find(scope => scope.type === 'module');
|
|
2876
|
-
const preconnectBinding = moduleScope?.set.get(preconnectImport.local.name);
|
|
2877
|
-
assert.ok(preconnectBinding, 'shared preconnect source binding must be resolvable');
|
|
2878
|
-
|
|
2879
|
-
const providerClass = compatAst.body
|
|
2880
|
-
.map(node => node.type === 'ExportNamedDeclaration' ? node.declaration : node)
|
|
2881
|
-
.find(node =>
|
|
2882
|
-
node?.type === 'ClassDeclaration'
|
|
2883
|
-
&& node.id?.name === 'OpenAICompatProvider',
|
|
2884
|
-
);
|
|
2885
|
-
assert.ok(providerClass, 'OpenAICompatProvider class must be resolvable');
|
|
2886
|
-
const doSendMethod = providerClass.body.body.find(node =>
|
|
2887
|
-
node.type === 'MethodDefinition'
|
|
2888
|
-
&& node.key?.type === 'Identifier'
|
|
2889
|
-
&& node.key.name === '_doSend',
|
|
2890
|
-
);
|
|
2891
|
-
assert.ok(doSendMethod, 'OpenAICompatProvider._doSend must be resolvable');
|
|
2892
|
-
|
|
2893
|
-
let callsInjectedInstanceMember = false;
|
|
2894
|
-
const visit = node => {
|
|
2895
|
-
if (!node || typeof node !== 'object') return;
|
|
2896
|
-
if (
|
|
2897
|
-
node.type === 'CallExpression'
|
|
2898
|
-
&& node.callee?.type === 'MemberExpression'
|
|
2899
|
-
&& node.callee.object?.type === 'ThisExpression'
|
|
2900
|
-
&& node.callee.computed === false
|
|
2901
|
-
&& node.callee.property?.name === '_preconnectFn'
|
|
2902
|
-
) {
|
|
2903
|
-
callsInjectedInstanceMember = true;
|
|
2904
|
-
}
|
|
2905
|
-
for (const [key, value] of Object.entries(node)) {
|
|
2906
|
-
if (key === 'start' || key === 'end') continue;
|
|
2907
|
-
if (Array.isArray(value)) value.forEach(visit);
|
|
2908
|
-
else if (value && typeof value === 'object') visit(value);
|
|
2909
|
-
}
|
|
2910
|
-
};
|
|
2911
|
-
visit(doSendMethod.value.body);
|
|
2912
|
-
const importedBindingReferences = preconnectBinding.references.filter(reference =>
|
|
2913
|
-
reference.identifier.start >= doSendMethod.value.body.start
|
|
2914
|
-
&& reference.identifier.end <= doSendMethod.value.body.end,
|
|
2915
|
-
);
|
|
2916
|
-
assert.equal(
|
|
2917
|
-
importedBindingReferences.length,
|
|
2918
|
-
0,
|
|
2919
|
-
'_doSend must not reference the shared preconnect import binding',
|
|
2920
|
-
);
|
|
2921
|
-
assert.equal(
|
|
2922
|
-
callsInjectedInstanceMember,
|
|
2923
|
-
true,
|
|
2924
|
-
'_doSend must call the instance preconnect seam',
|
|
2925
|
-
);
|
|
2926
|
-
|
|
2927
|
-
const provider = new OpenAICompatProvider('xai', {
|
|
2928
|
-
apiKey: 'xai-test',
|
|
2929
|
-
responsesTransport: 'http',
|
|
2930
|
-
preconnect: true,
|
|
2931
|
-
preconnectFn: () => {
|
|
2932
|
-
injectedPreconnectCalls += 1;
|
|
2933
|
-
},
|
|
2934
|
-
});
|
|
2935
|
-
let httpCalled = false;
|
|
2936
|
-
provider._doSendXaiResponses = async () => {
|
|
2937
|
-
httpCalled = true;
|
|
2938
|
-
return { content: 'ok' };
|
|
2939
|
-
};
|
|
2940
|
-
provider._doSendXaiResponsesWebSocket = async () => {
|
|
2941
|
-
throw new Error('explicit HTTP transport should not use WS');
|
|
2942
|
-
};
|
|
2943
|
-
const result = await provider._doSend([{ role: 'user', content: 'hi' }], 'grok-build', [], {});
|
|
2944
|
-
assert.equal(result.content, 'ok');
|
|
2945
|
-
assert.equal(httpCalled, true);
|
|
2946
|
-
assert.equal(injectedPreconnectCalls, 1, 'only the injected preconnect seam must run');
|
|
2947
|
-
assert.equal(outboundFetchAttempts, 0, 'stubbed provider test must remain network hermetic');
|
|
2948
|
-
} finally {
|
|
2949
|
-
globalThis.fetch = prevFetch;
|
|
2950
|
-
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2951
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2952
|
-
}
|
|
2953
|
-
});
|
|
2954
|
-
|
|
2955
|
-
test('grok-oauth: every OAuth model is pinned to the CLI proxy over HTTP/SSE', () => {
|
|
2956
|
-
const prevOaiTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2957
|
-
const prevResponsesTransport = process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT;
|
|
2958
|
-
const prevGrokTransport = process.env.MIXDOG_GROK_OAUTH_TRANSPORT;
|
|
2959
|
-
try {
|
|
2960
|
-
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2961
|
-
delete process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT;
|
|
2962
|
-
delete process.env.MIXDOG_GROK_OAUTH_TRANSPORT;
|
|
2963
|
-
const provider = new GrokOAuthProvider({});
|
|
2964
|
-
|
|
2965
|
-
for (const model of ['grok-build-0.1', 'grok-build', 'grok-4.5']) {
|
|
2966
|
-
const inner = provider._ensureInner(`tok-${model}`, model);
|
|
2967
|
-
assert.equal(inner.config.responsesTransport, 'http');
|
|
2968
|
-
assert.equal(inner.baseURL, 'https://cli-chat-proxy.grok.com/v1');
|
|
2969
|
-
}
|
|
2970
|
-
|
|
2971
|
-
// OAuth routing is a security boundary: even explicit WS settings
|
|
2972
|
-
// cannot send the session bearer to the fixed api.x.ai WS endpoint.
|
|
2973
|
-
process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT = 'websocket';
|
|
2974
|
-
const pinnedProvider = new GrokOAuthProvider({ responsesTransport: 'websocket' });
|
|
2975
|
-
const pinned = pinnedProvider._ensureInner('tok-explicit', 'grok-4.5');
|
|
2976
|
-
assert.equal(pinned.config.responsesTransport, 'http');
|
|
2977
|
-
assert.equal(pinned.baseURL, 'https://cli-chat-proxy.grok.com/v1');
|
|
2978
|
-
} finally {
|
|
2979
|
-
if (prevOaiTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2980
|
-
else process.env.MIXDOG_OAI_TRANSPORT = prevOaiTransport;
|
|
2981
|
-
if (prevResponsesTransport == null) delete process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT;
|
|
2982
|
-
else process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT = prevResponsesTransport;
|
|
2983
|
-
if (prevGrokTransport == null) delete process.env.MIXDOG_GROK_OAUTH_TRANSPORT;
|
|
2984
|
-
else process.env.MIXDOG_GROK_OAUTH_TRANSPORT = prevGrokTransport;
|
|
2985
|
-
}
|
|
2986
|
-
});
|
|
2987
|
-
|
|
2988
|
-
test('responses transport policy: _gateTransportMode down-shifts per capability', () => {
|
|
2989
|
-
// delta unsupported → ws-delta collapses to ws-full; others pass through.
|
|
2990
|
-
const noDelta = { ws: true, http: true, delta: false };
|
|
2991
|
-
assert.equal(_gateTransportMode('auto', noDelta), 'ws-full');
|
|
2992
|
-
assert.equal(_gateTransportMode('ws-delta', noDelta), 'ws-full');
|
|
2993
|
-
assert.equal(_gateTransportMode('ws-full', noDelta), 'ws-full');
|
|
2994
|
-
assert.equal(_gateTransportMode('http-sse', noDelta), 'http-sse');
|
|
2995
|
-
// WS unsupported → WS modes prefer HTTP.
|
|
2996
|
-
const httpOnly = { ws: false, http: true, delta: false };
|
|
2997
|
-
assert.equal(_gateTransportMode('auto', httpOnly), 'http-sse');
|
|
2998
|
-
assert.equal(_gateTransportMode('ws-full', httpOnly), 'http-sse');
|
|
2999
|
-
assert.equal(_gateTransportMode('ws-delta', httpOnly), 'http-sse');
|
|
3000
|
-
// HTTP unsupported → http-sse prefers full-frame WS.
|
|
3001
|
-
const wsOnly = { ws: true, http: false, delta: true };
|
|
3002
|
-
assert.equal(_gateTransportMode('http-sse', wsOnly), 'ws-full');
|
|
3003
|
-
// full caps pass everything through unchanged.
|
|
3004
|
-
for (const m of ['auto', 'ws-full', 'ws-delta', 'http-sse']) {
|
|
3005
|
-
assert.equal(_gateTransportMode(m, FULL_RESPONSES_TRANSPORT_CAPS), m);
|
|
3006
|
-
}
|
|
3007
|
-
});
|
|
3008
|
-
|
|
3009
|
-
// === 11. Codex x-codex-turn-state parity =================================
|
|
3010
|
-
// Server-issued sticky-routing token. codex-rs stores it in a per-turn
|
|
3011
|
-
// `OnceLock<String>` (client.rs:277-285): captured ONCE at turn start from the
|
|
3012
|
-
// `x-codex-turn-state` RESPONSE header, replayed unchanged on every request
|
|
3013
|
-
// within that turn, never fabricated, and dropped between turns (a fresh
|
|
3014
|
-
// ModelClientSession/OnceLock per turn). These tests pin that exact contract
|
|
3015
|
-
// against our pooled-socket emulation (capture + first-use/turn-id attribution
|
|
3016
|
-
// + per-turn drop guard).
|
|
3017
|
-
test('codex turn-state: captures server response header once, never synthesizes', () => {
|
|
3018
|
-
const entry = {};
|
|
3019
|
-
// No header on the event → nothing captured (never fabricated).
|
|
3020
|
-
_captureTurnStateFromEvent(entry, { type: 'response.created', headers: {} });
|
|
3021
|
-
assert.equal(entry.turnState, undefined);
|
|
3022
|
-
// Server issues the token on a response header → captured.
|
|
3023
|
-
_captureTurnStateFromEvent(entry, { type: 'response.created', headers: { 'x-codex-turn-state': 'tok-1' } });
|
|
3024
|
-
assert.equal(entry.turnState, 'tok-1');
|
|
3025
|
-
// OnceLock semantics: a later server token in the same turn does NOT overwrite.
|
|
3026
|
-
_captureTurnStateFromEvent(entry, { headers: { 'x-codex-turn-state': 'tok-2' } });
|
|
3027
|
-
assert.equal(entry.turnState, 'tok-1');
|
|
3028
|
-
});
|
|
3029
|
-
|
|
3030
|
-
test('codex turn-state: captures from nested response/metadata header shapes', () => {
|
|
3031
|
-
const fromResponse = {};
|
|
3032
|
-
_captureTurnStateFromEvent(fromResponse, { response: { headers: { 'x-codex-turn-state': 'tok-r' } } });
|
|
3033
|
-
assert.equal(fromResponse.turnState, 'tok-r');
|
|
3034
|
-
const fromMeta = {};
|
|
3035
|
-
_captureTurnStateFromEvent(fromMeta, { response: { metadata: { headers: { 'x-codex-turn-state': 'tok-m' } } } });
|
|
3036
|
-
assert.equal(fromMeta.turnState, 'tok-m');
|
|
3037
|
-
});
|
|
3038
|
-
|
|
3039
|
-
test('codex turn-state: echoed within a turn, dropped across turns, never fabricated', () => {
|
|
3040
|
-
const ctxA = { sendOpts: { turnId: 'turn-A', codexSessionId: 'sess', threadId: 'thread' } };
|
|
3041
|
-
// A server-captured token with unknown owner (handshake/prewarm capture).
|
|
3042
|
-
const entry = { turnState: 'tok-A' };
|
|
3043
|
-
const f1 = _withCodexWsClientMetadata({}, entry, true, ctxA);
|
|
3044
|
-
assert.equal(f1.client_metadata['x-codex-turn-state'], 'tok-A');
|
|
3045
|
-
// First use attributes the token to the turn now on the wire.
|
|
3046
|
-
assert.equal(entry.turnStateTurnId, 'turn-A');
|
|
3047
|
-
// Subsequent request in the SAME turn replays the same token.
|
|
3048
|
-
const f2 = _withCodexWsClientMetadata({}, entry, true, ctxA);
|
|
3049
|
-
assert.equal(f2.client_metadata['x-codex-turn-state'], 'tok-A');
|
|
3050
|
-
// Next turn: the token must be dropped, never replayed or fabricated.
|
|
3051
|
-
const ctxB = { sendOpts: { turnId: 'turn-B', codexSessionId: 'sess', threadId: 'thread' } };
|
|
3052
|
-
const f3 = _withCodexWsClientMetadata({}, entry, true, ctxB);
|
|
3053
|
-
assert.equal('x-codex-turn-state' in f3.client_metadata, false);
|
|
3054
|
-
assert.equal(entry.turnState, null);
|
|
3055
|
-
});
|
|
3056
|
-
|
|
3057
|
-
test('codex turn-state: parity disabled leaves the frame untouched (no metadata, no echo)', () => {
|
|
3058
|
-
const entry = { turnState: 'tok-A' };
|
|
3059
|
-
const frame = { input: [] };
|
|
3060
|
-
const out = _withCodexWsClientMetadata(frame, entry, false, { sendOpts: { turnId: 'turn-A' } });
|
|
3061
|
-
assert.equal(out, frame);
|
|
3062
|
-
assert.equal(out.client_metadata, undefined);
|
|
3063
|
-
});
|