mixdog 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,1467 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { loadConfig } from '../config.mjs';
|
|
4
|
+
import { withRetry } from './retry-classifier.mjs';
|
|
5
|
+
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
6
|
+
import { appendBridgeTrace, traceBridgeUsage } from '../bridge-trace.mjs';
|
|
7
|
+
import { resolveProviderCacheKey } from '../smart-bridge/cache-strategy.mjs';
|
|
8
|
+
import {
|
|
9
|
+
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
10
|
+
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
11
|
+
createTimeoutSignal,
|
|
12
|
+
resolveTimeoutMs,
|
|
13
|
+
} from '../stall-policy.mjs';
|
|
14
|
+
// OPENAI_COMPAT_PRESETS — self-declaring list of compat provider names and
|
|
15
|
+
// their base URLs / defaults. registry.mjs imports this export so there is no
|
|
16
|
+
// parallel OPENAI_COMPAT_PROVIDERS list to maintain separately.
|
|
17
|
+
export const OPENAI_COMPAT_PRESETS = {
|
|
18
|
+
deepseek: {
|
|
19
|
+
baseURL: 'https://api.deepseek.com',
|
|
20
|
+
defaultModel: 'deepseek-v4-pro',
|
|
21
|
+
},
|
|
22
|
+
xai: {
|
|
23
|
+
baseURL: 'https://api.x.ai/v1',
|
|
24
|
+
defaultModel: 'grok-4.3',
|
|
25
|
+
},
|
|
26
|
+
nvidia: {
|
|
27
|
+
baseURL: 'https://integrate.api.nvidia.com/v1',
|
|
28
|
+
defaultModel: 'meta/llama-3.3-70b-instruct',
|
|
29
|
+
},
|
|
30
|
+
ollama: {
|
|
31
|
+
baseURL: 'http://localhost:11434/v1',
|
|
32
|
+
defaultModel: 'llama3.3:latest',
|
|
33
|
+
},
|
|
34
|
+
lmstudio: {
|
|
35
|
+
baseURL: 'http://localhost:1234/v1',
|
|
36
|
+
defaultModel: 'default',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const PRESETS = OPENAI_COMPAT_PRESETS;
|
|
40
|
+
const MODEL_LIST_TIMEOUT_MS = resolveTimeoutMs(
|
|
41
|
+
'MIXDOG_COMPAT_MODEL_LIST_TIMEOUT_MS',
|
|
42
|
+
10_000,
|
|
43
|
+
{ minMs: 1_000, maxMs: PROVIDER_GENERATE_TOTAL_TIMEOUT_MS },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// SSRF guard for provider baseURL. config.baseURL comes from user JSON;
|
|
47
|
+
// reject non-http(s) schemes (file:/data:/ftp:/etc.) and require https for
|
|
48
|
+
// any non-localhost host. Localhost-only presets (ollama, lmstudio) and
|
|
49
|
+
// loopback hosts may use http. Throws a clear config error — no silent
|
|
50
|
+
// fallback — so misconfig surfaces immediately instead of leaking apiKey.
|
|
51
|
+
function assertSafeBaseURL(rawURL, providerName) {
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = new URL(String(rawURL));
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(`[provider:${providerName}] invalid baseURL: ${rawURL}`);
|
|
57
|
+
}
|
|
58
|
+
const scheme = parsed.protocol.toLowerCase();
|
|
59
|
+
if (scheme !== 'https:' && scheme !== 'http:') {
|
|
60
|
+
throw new Error(`[provider:${providerName}] baseURL scheme not allowed: ${parsed.protocol} (only http/https)`);
|
|
61
|
+
}
|
|
62
|
+
if (scheme === 'http:') {
|
|
63
|
+
const host = parsed.hostname.toLowerCase();
|
|
64
|
+
const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
65
|
+
if (!isLocal) {
|
|
66
|
+
throw new Error(`[provider:${providerName}] baseURL must use https for non-localhost host (got ${parsed.protocol}//${parsed.hostname})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return rawURL;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function traceHash(value) {
|
|
73
|
+
return createHash('sha256')
|
|
74
|
+
.update(String(value ?? ''))
|
|
75
|
+
.digest('hex')
|
|
76
|
+
.slice(0, 16);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stableTraceStringify(value, seen = new WeakSet()) {
|
|
80
|
+
if (value === null || typeof value !== 'object') {
|
|
81
|
+
if (typeof value === 'bigint') return JSON.stringify(String(value));
|
|
82
|
+
if (typeof value === 'undefined' || typeof value === 'function') return 'null';
|
|
83
|
+
return JSON.stringify(value);
|
|
84
|
+
}
|
|
85
|
+
if (seen.has(value)) return JSON.stringify('[Circular]');
|
|
86
|
+
seen.add(value);
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
const serialized = '[' + value.map(v => stableTraceStringify(v, seen)).join(',') + ']';
|
|
89
|
+
seen.delete(value);
|
|
90
|
+
return serialized;
|
|
91
|
+
}
|
|
92
|
+
const parts = [];
|
|
93
|
+
for (const key of Object.keys(value).sort()) {
|
|
94
|
+
const v = value[key];
|
|
95
|
+
if (typeof v === 'undefined' || typeof v === 'function') continue;
|
|
96
|
+
parts.push(JSON.stringify(key) + ':' + stableTraceStringify(v, seen));
|
|
97
|
+
}
|
|
98
|
+
seen.delete(value);
|
|
99
|
+
return '{' + parts.join(',') + '}';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function traceTextShape(text) {
|
|
103
|
+
const value = String(text ?? '');
|
|
104
|
+
return { chars: value.length, hash: traceHash(value) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function summarizeTraceMessages(messages) {
|
|
108
|
+
const summaries = (messages || []).map((m, index) => {
|
|
109
|
+
const content = typeof m?.content === 'string'
|
|
110
|
+
? { type: 'text', ...traceTextShape(m.content) }
|
|
111
|
+
: { type: m?.content == null ? 'null' : typeof m.content, hash: traceHash(stableTraceStringify(m?.content ?? null)) };
|
|
112
|
+
const toolCalls = Array.isArray(m?.tool_calls)
|
|
113
|
+
? m.tool_calls.map(tc => ({
|
|
114
|
+
name: tc?.function?.name || null,
|
|
115
|
+
argsHash: traceHash(tc?.function?.arguments || ''),
|
|
116
|
+
}))
|
|
117
|
+
: [];
|
|
118
|
+
return {
|
|
119
|
+
index,
|
|
120
|
+
role: m?.role || null,
|
|
121
|
+
content,
|
|
122
|
+
...(typeof m?.reasoning_content === 'string'
|
|
123
|
+
? { reasoningContent: traceTextShape(m.reasoning_content) }
|
|
124
|
+
: {}),
|
|
125
|
+
toolCallCount: toolCalls.length,
|
|
126
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
if (summaries.length <= 12) return summaries;
|
|
130
|
+
return [
|
|
131
|
+
...summaries.slice(0, 8),
|
|
132
|
+
{ omittedTurns: summaries.length - 12 },
|
|
133
|
+
...summaries.slice(-4),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function summarizeTraceTools(tools) {
|
|
138
|
+
return (tools || []).map(t => ({
|
|
139
|
+
name: t?.name || null,
|
|
140
|
+
description: t?.description || '',
|
|
141
|
+
inputSchema: t?.inputSchema || null,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function extractCompatCachedTokens(usage) {
|
|
146
|
+
const candidates = [
|
|
147
|
+
usage?.prompt_tokens_details?.cached_tokens,
|
|
148
|
+
usage?.input_tokens_details?.cached_tokens,
|
|
149
|
+
usage?.prompt_cache_hit_tokens,
|
|
150
|
+
usage?.cached_prompt_text_tokens,
|
|
151
|
+
];
|
|
152
|
+
for (const v of candidates) {
|
|
153
|
+
const n = Number(v);
|
|
154
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
155
|
+
}
|
|
156
|
+
for (const v of candidates) {
|
|
157
|
+
const n = Number(v);
|
|
158
|
+
if (Number.isFinite(n)) return n;
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function xaiPrefixSeed({ opts, params, rawTools, model }) {
|
|
164
|
+
const providerKey = resolveProviderCacheKey(opts, 'xai');
|
|
165
|
+
const systemMessages = (params?.messages || [])
|
|
166
|
+
.filter(m => m?.role === 'system')
|
|
167
|
+
.map(m => String(m?.content ?? ''));
|
|
168
|
+
return stableTraceStringify({
|
|
169
|
+
scope: 'xai-prefix-model-system-tools',
|
|
170
|
+
providerKey: String(providerKey),
|
|
171
|
+
model: model || null,
|
|
172
|
+
systemMessages,
|
|
173
|
+
tools: summarizeTraceTools(rawTools),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function xaiCacheRouting(opts, params, rawTools, model) {
|
|
178
|
+
const sessionId = String(opts?.sessionId || opts?.session?.id || '').trim();
|
|
179
|
+
const providerKey = resolveProviderCacheKey(opts, 'xai');
|
|
180
|
+
const prefixSeed = xaiPrefixSeed({ opts, params, rawTools, model });
|
|
181
|
+
const prefixHash = traceHash(prefixSeed);
|
|
182
|
+
const routingSeed = stableTraceStringify({
|
|
183
|
+
scope: 'xai-chat-session-v1',
|
|
184
|
+
providerKey: String(providerKey),
|
|
185
|
+
model: model || null,
|
|
186
|
+
sessionId: sessionId || `ephemeral:${process.pid}`,
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
key: deterministicUuidFromKey(routingSeed),
|
|
190
|
+
mode: sessionId ? 'session' : 'ephemeral',
|
|
191
|
+
seedHash: traceHash(routingSeed),
|
|
192
|
+
prefixHash,
|
|
193
|
+
ownerSessionHash: sessionId ? traceHash(sessionId) : null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function xaiResponsesCacheRouting(opts, params, rawTools, model) {
|
|
198
|
+
// Default to 'prefix' so parallel workers sharing the same model + system
|
|
199
|
+
// + tools land on a common prompt_cache_key, letting xAI's server-side
|
|
200
|
+
// prefix cache hit across sessions instead of cold-starting per worker.
|
|
201
|
+
// Override with 'session' (env or opts) for legacy session-isolated lanes.
|
|
202
|
+
const scope = String(opts?.xaiResponsesCacheScope || process.env.MIXDOG_XAI_RESPONSES_CACHE_SCOPE || 'prefix')
|
|
203
|
+
.trim()
|
|
204
|
+
.toLowerCase();
|
|
205
|
+
if (scope !== 'prefix') {
|
|
206
|
+
return xaiCacheRouting(opts, params, rawTools, model);
|
|
207
|
+
}
|
|
208
|
+
const sessionId = String(opts?.sessionId || opts?.session?.id || '').trim();
|
|
209
|
+
const providerKey = resolveProviderCacheKey(opts, 'xai');
|
|
210
|
+
const prefixSeed = xaiPrefixSeed({ opts, params, rawTools, model });
|
|
211
|
+
const prefixHash = traceHash(prefixSeed);
|
|
212
|
+
const routingSeed = stableTraceStringify({
|
|
213
|
+
scope: 'xai-responses-prefix-v1',
|
|
214
|
+
providerKey: String(providerKey),
|
|
215
|
+
model: model || null,
|
|
216
|
+
prefixHash,
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
key: deterministicUuidFromKey(routingSeed),
|
|
220
|
+
mode: 'prefix',
|
|
221
|
+
seedHash: traceHash(routingSeed),
|
|
222
|
+
prefixHash,
|
|
223
|
+
ownerSessionHash: sessionId ? traceHash(sessionId) : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeXaiReasoningEffort(value) {
|
|
228
|
+
const effort = String(value || '').trim().toLowerCase();
|
|
229
|
+
return ['none', 'low', 'medium', 'high'].includes(effort) ? effort : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function useXaiResponsesApi(opts, config) {
|
|
233
|
+
const raw = opts?.xaiApiMode
|
|
234
|
+
?? config?.apiMode
|
|
235
|
+
?? config?.xaiApiMode
|
|
236
|
+
?? process.env.MIXDOG_XAI_API_MODE
|
|
237
|
+
?? process.env.MIXDOG_XAI_RESPONSES;
|
|
238
|
+
if (raw == null || raw === '') return true;
|
|
239
|
+
const mode = String(raw).trim().toLowerCase();
|
|
240
|
+
return !['0', 'false', 'off', 'chat', 'chat-completions', 'chat_completions'].includes(mode);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function useXaiResponsesWebSocket(opts, config) {
|
|
244
|
+
const raw = opts?.xaiResponsesTransport
|
|
245
|
+
?? opts?.xaiTransport
|
|
246
|
+
?? config?.responsesTransport
|
|
247
|
+
?? config?.transport
|
|
248
|
+
?? process.env.MIXDOG_XAI_RESPONSES_TRANSPORT
|
|
249
|
+
?? process.env.MIXDOG_XAI_TRANSPORT;
|
|
250
|
+
if (raw == null || raw === '') return true;
|
|
251
|
+
const transport = String(raw).trim().toLowerCase();
|
|
252
|
+
return !['0', 'false', 'off', 'http', 'https', 'responses-http', 'sdk'].includes(transport);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function useXaiResponsesWebSocketWarmup(opts, config, { previousResponseId, instructions, rawTools }) {
|
|
256
|
+
if (previousResponseId) return false;
|
|
257
|
+
const raw = opts?.xaiResponsesWarmup
|
|
258
|
+
?? opts?.xaiWsWarmup
|
|
259
|
+
?? config?.responsesWarmup
|
|
260
|
+
?? config?.wsWarmup
|
|
261
|
+
?? process.env.MIXDOG_XAI_RESPONSES_WARMUP
|
|
262
|
+
?? process.env.MIXDOG_XAI_WS_WARMUP;
|
|
263
|
+
if (raw != null && raw !== '') {
|
|
264
|
+
const mode = String(raw).trim().toLowerCase();
|
|
265
|
+
if (['0', 'false', 'off', 'none', 'disabled'].includes(mode)) return false;
|
|
266
|
+
if (['1', 'true', 'on', 'always', 'force'].includes(mode)) return true;
|
|
267
|
+
}
|
|
268
|
+
return String(instructions || '').length >= 2048 || (Array.isArray(rawTools) && rawTools.length >= 10);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const XAI_RESPONSES_CACHE_LANE_DEFAULT_MAX_IN_FLIGHT = 2;
|
|
272
|
+
const xaiResponsesCacheLanes = new Map();
|
|
273
|
+
|
|
274
|
+
function parseXaiPositiveInt(value, fallback) {
|
|
275
|
+
if (value == null || value === '') return fallback;
|
|
276
|
+
const text = String(value).trim().toLowerCase();
|
|
277
|
+
if (['0', 'false', 'off', 'none', 'disabled', 'unlimited'].includes(text)) return 0;
|
|
278
|
+
const n = Number(text);
|
|
279
|
+
if (!Number.isFinite(n)) return fallback;
|
|
280
|
+
return Math.max(0, Math.floor(n));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function xaiResponsesCacheLaneMaxInFlight(opts, config) {
|
|
284
|
+
return parseXaiPositiveInt(
|
|
285
|
+
opts?.xaiCacheMaxInFlight
|
|
286
|
+
?? opts?.xaiResponsesCacheMaxInFlight
|
|
287
|
+
?? config?.xaiCacheMaxInFlight
|
|
288
|
+
?? config?.xaiResponsesCacheMaxInFlight
|
|
289
|
+
?? process.env.MIXDOG_XAI_CACHE_MAX_INFLIGHT
|
|
290
|
+
?? process.env.MIXDOG_XAI_RESPONSES_CACHE_MAX_INFLIGHT,
|
|
291
|
+
XAI_RESPONSES_CACHE_LANE_DEFAULT_MAX_IN_FLIGHT,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function xaiResponsesCacheLaneQueueTimeoutMs(opts, config) {
|
|
296
|
+
return parseXaiPositiveInt(
|
|
297
|
+
opts?.xaiCacheQueueTimeoutMs
|
|
298
|
+
?? opts?.xaiResponsesCacheQueueTimeoutMs
|
|
299
|
+
?? config?.xaiCacheQueueTimeoutMs
|
|
300
|
+
?? config?.xaiResponsesCacheQueueTimeoutMs
|
|
301
|
+
?? process.env.MIXDOG_XAI_CACHE_QUEUE_TIMEOUT_MS
|
|
302
|
+
?? process.env.MIXDOG_XAI_RESPONSES_CACHE_QUEUE_TIMEOUT_MS,
|
|
303
|
+
0,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function xaiResponsesCacheLaneShard(opts, cacheRouting) {
|
|
308
|
+
const shardCount = parseXaiPositiveInt(
|
|
309
|
+
opts?.xaiCacheLaneShards
|
|
310
|
+
?? opts?.xaiResponsesCacheLaneShards
|
|
311
|
+
?? process.env.MIXDOG_XAI_CACHE_LANE_SHARDS
|
|
312
|
+
?? process.env.MIXDOG_XAI_RESPONSES_CACHE_LANE_SHARDS,
|
|
313
|
+
1,
|
|
314
|
+
);
|
|
315
|
+
if (shardCount <= 1) return 0;
|
|
316
|
+
const seed = String(opts?.sessionId || opts?.session?.id || cacheRouting?.ownerSessionHash || cacheRouting?.key || '');
|
|
317
|
+
const hex = traceHash(seed || 'xai-cache-lane');
|
|
318
|
+
return Number.parseInt(hex.slice(0, 8), 16) % shardCount;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function xaiResponsesCacheLaneKey({ model, cacheRouting, opts }) {
|
|
322
|
+
const prefix = cacheRouting?.prefixHash || cacheRouting?.seedHash || cacheRouting?.key || 'unknown-prefix';
|
|
323
|
+
const shard = xaiResponsesCacheLaneShard(opts, cacheRouting);
|
|
324
|
+
return {
|
|
325
|
+
key: `xai-responses:${model || 'default'}:${prefix}:shard-${shard}`,
|
|
326
|
+
shard,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getXaiResponsesCacheLaneState(key, maxInFlight) {
|
|
331
|
+
let state = xaiResponsesCacheLanes.get(key);
|
|
332
|
+
if (!state) {
|
|
333
|
+
state = { key, active: 0, queue: [], maxInFlight, nextId: 0 };
|
|
334
|
+
xaiResponsesCacheLanes.set(key, state);
|
|
335
|
+
}
|
|
336
|
+
state.maxInFlight = maxInFlight;
|
|
337
|
+
return state;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function cleanupXaiResponsesCacheLane(state) {
|
|
341
|
+
if (state.active === 0 && state.queue.length === 0) {
|
|
342
|
+
xaiResponsesCacheLanes.delete(state.key);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function removeQueuedXaiCacheLaneRequest(state, request) {
|
|
347
|
+
const index = state.queue.indexOf(request);
|
|
348
|
+
if (index >= 0) state.queue.splice(index, 1);
|
|
349
|
+
cleanupXaiResponsesCacheLane(state);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function makeXaiCacheLaneHandle(state, requestId, enqueuedAt) {
|
|
353
|
+
let released = false;
|
|
354
|
+
return {
|
|
355
|
+
requestId,
|
|
356
|
+
waitedMs: Date.now() - enqueuedAt,
|
|
357
|
+
activeCount: state.active,
|
|
358
|
+
queueDepth: state.queue.length,
|
|
359
|
+
release() {
|
|
360
|
+
if (released) return;
|
|
361
|
+
released = true;
|
|
362
|
+
releaseXaiResponsesCacheLane(state);
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function releaseXaiResponsesCacheLane(state) {
|
|
368
|
+
state.active = Math.max(0, state.active - 1);
|
|
369
|
+
while (state.queue.length > 0 && state.active < state.maxInFlight) {
|
|
370
|
+
const next = state.queue.shift();
|
|
371
|
+
next.cleanup?.();
|
|
372
|
+
state.active += 1;
|
|
373
|
+
next.resolve(makeXaiCacheLaneHandle(state, next.requestId, next.enqueuedAt));
|
|
374
|
+
}
|
|
375
|
+
cleanupXaiResponsesCacheLane(state);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function acquireXaiResponsesCacheLane({ key, maxInFlight, signal, timeoutMs }) {
|
|
379
|
+
const state = getXaiResponsesCacheLaneState(key, maxInFlight);
|
|
380
|
+
const requestId = ++state.nextId;
|
|
381
|
+
const enqueuedAt = Date.now();
|
|
382
|
+
if (state.active < state.maxInFlight) {
|
|
383
|
+
state.active += 1;
|
|
384
|
+
return Promise.resolve(makeXaiCacheLaneHandle(state, requestId, enqueuedAt));
|
|
385
|
+
}
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const request = {
|
|
388
|
+
requestId,
|
|
389
|
+
enqueuedAt,
|
|
390
|
+
resolve,
|
|
391
|
+
reject,
|
|
392
|
+
cleanup: null,
|
|
393
|
+
};
|
|
394
|
+
const cleanup = () => {
|
|
395
|
+
if (request.timer) clearTimeout(request.timer);
|
|
396
|
+
if (signal && request.abortListener) signal.removeEventListener('abort', request.abortListener);
|
|
397
|
+
};
|
|
398
|
+
request.cleanup = cleanup;
|
|
399
|
+
request.abortListener = () => {
|
|
400
|
+
cleanup();
|
|
401
|
+
removeQueuedXaiCacheLaneRequest(state, request);
|
|
402
|
+
const reason = signal?.reason;
|
|
403
|
+
reject(reason instanceof Error ? reason : new Error('xAI cache lane wait aborted'));
|
|
404
|
+
};
|
|
405
|
+
if (signal?.aborted) {
|
|
406
|
+
request.abortListener();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (signal) signal.addEventListener('abort', request.abortListener, { once: true });
|
|
410
|
+
if (timeoutMs > 0) {
|
|
411
|
+
request.timer = setTimeout(() => {
|
|
412
|
+
cleanup();
|
|
413
|
+
removeQueuedXaiCacheLaneRequest(state, request);
|
|
414
|
+
reject(new Error(`xAI cache lane wait timed out after ${timeoutMs}ms`));
|
|
415
|
+
}, timeoutMs);
|
|
416
|
+
}
|
|
417
|
+
state.queue.push(request);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function traceXaiCacheLane(opts, payload) {
|
|
422
|
+
if (!compatCacheTraceEnabled('xai')) return;
|
|
423
|
+
try {
|
|
424
|
+
appendBridgeTrace({
|
|
425
|
+
sessionId: opts?.sessionId || opts?.session?.id || null,
|
|
426
|
+
iteration: Number.isFinite(Number(opts?.iteration)) ? Number(opts.iteration) : null,
|
|
427
|
+
kind: 'cache_lane',
|
|
428
|
+
...payload,
|
|
429
|
+
payload,
|
|
430
|
+
});
|
|
431
|
+
} catch {}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function withXaiResponsesCacheLane({ opts, config, cacheRouting, model, transport, previousResponseId, inputCount, signal }, fn) {
|
|
435
|
+
const maxInFlight = xaiResponsesCacheLaneMaxInFlight(opts, config);
|
|
436
|
+
if (maxInFlight <= 0) {
|
|
437
|
+
const laneMeta = { enabled: false, maxInFlight: 0 };
|
|
438
|
+
return { value: await fn(laneMeta), laneMeta };
|
|
439
|
+
}
|
|
440
|
+
const { key: laneKey, shard } = xaiResponsesCacheLaneKey({ model, cacheRouting, opts });
|
|
441
|
+
const timeoutMs = xaiResponsesCacheLaneQueueTimeoutMs(opts, config);
|
|
442
|
+
const state = getXaiResponsesCacheLaneState(laneKey, maxInFlight);
|
|
443
|
+
const queued = state.active >= state.maxInFlight;
|
|
444
|
+
if (queued) {
|
|
445
|
+
traceXaiCacheLane(opts, {
|
|
446
|
+
provider: 'xai',
|
|
447
|
+
api: 'responses',
|
|
448
|
+
transport,
|
|
449
|
+
event: 'queued',
|
|
450
|
+
lane_key_hash: traceHash(laneKey),
|
|
451
|
+
lane_shard: shard,
|
|
452
|
+
max_in_flight: maxInFlight,
|
|
453
|
+
active: state.active,
|
|
454
|
+
queue_depth: state.queue.length,
|
|
455
|
+
previous_response_used: !!previousResponseId,
|
|
456
|
+
input_count: inputCount,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const handle = await acquireXaiResponsesCacheLane({ key: laneKey, maxInFlight, signal, timeoutMs });
|
|
460
|
+
const laneMeta = {
|
|
461
|
+
enabled: true,
|
|
462
|
+
laneKeyHash: traceHash(laneKey),
|
|
463
|
+
shard,
|
|
464
|
+
maxInFlight,
|
|
465
|
+
queued,
|
|
466
|
+
waitMs: handle.waitedMs,
|
|
467
|
+
activeAfterAcquire: handle.activeCount,
|
|
468
|
+
queueDepthAfterAcquire: handle.queueDepth,
|
|
469
|
+
};
|
|
470
|
+
traceXaiCacheLane(opts, {
|
|
471
|
+
provider: 'xai',
|
|
472
|
+
api: 'responses',
|
|
473
|
+
transport,
|
|
474
|
+
event: 'acquired',
|
|
475
|
+
lane_key_hash: laneMeta.laneKeyHash,
|
|
476
|
+
lane_shard: shard,
|
|
477
|
+
max_in_flight: maxInFlight,
|
|
478
|
+
wait_ms: laneMeta.waitMs,
|
|
479
|
+
active: laneMeta.activeAfterAcquire,
|
|
480
|
+
queue_depth: laneMeta.queueDepthAfterAcquire,
|
|
481
|
+
previous_response_used: !!previousResponseId,
|
|
482
|
+
input_count: inputCount,
|
|
483
|
+
});
|
|
484
|
+
const startedAt = Date.now();
|
|
485
|
+
try {
|
|
486
|
+
return { value: await fn(laneMeta), laneMeta };
|
|
487
|
+
} finally {
|
|
488
|
+
handle.release();
|
|
489
|
+
traceXaiCacheLane(opts, {
|
|
490
|
+
provider: 'xai',
|
|
491
|
+
api: 'responses',
|
|
492
|
+
transport,
|
|
493
|
+
event: 'released',
|
|
494
|
+
lane_key_hash: laneMeta.laneKeyHash,
|
|
495
|
+
lane_shard: shard,
|
|
496
|
+
max_in_flight: maxInFlight,
|
|
497
|
+
held_ms: Date.now() - startedAt,
|
|
498
|
+
previous_response_used: !!previousResponseId,
|
|
499
|
+
input_count: inputCount,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function deterministicUuidFromKey(key) {
|
|
505
|
+
const hex = createHash('sha256').update(String(key ?? '')).digest('hex');
|
|
506
|
+
const variant = ((Number.parseInt(hex[16], 16) & 0x3) | 0x8).toString(16);
|
|
507
|
+
return [
|
|
508
|
+
hex.slice(0, 8),
|
|
509
|
+
hex.slice(8, 12),
|
|
510
|
+
'4' + hex.slice(13, 16),
|
|
511
|
+
variant + hex.slice(17, 20),
|
|
512
|
+
hex.slice(20, 32),
|
|
513
|
+
].join('-');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function compatCacheTraceEnabled(provider) {
|
|
517
|
+
return process.env.MIXDOG_COMPAT_CACHE_TRACE === '1'
|
|
518
|
+
|| process.env.MIXDOG_PROVIDER_CACHE_TRACE === '1'
|
|
519
|
+
|| (provider === 'xai' && process.env.MIXDOG_XAI_CACHE_TRACE === '1');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function writeCompatCacheTrace({ provider, model, opts, params, rawTools, response, cacheRoutingKey, cacheRouting }) {
|
|
523
|
+
if (!compatCacheTraceEnabled(provider)) return;
|
|
524
|
+
try {
|
|
525
|
+
const usage = response?.usage || {};
|
|
526
|
+
const inputTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
|
|
527
|
+
const cachedTokens = extractCompatCachedTokens(usage);
|
|
528
|
+
const toolShape = summarizeTraceTools(rawTools);
|
|
529
|
+
const traceMessages = Array.isArray(params?.messages) ? params.messages : [];
|
|
530
|
+
const trace = {
|
|
531
|
+
event: 'chat.completions',
|
|
532
|
+
provider,
|
|
533
|
+
model,
|
|
534
|
+
responseModel: response?.model || null,
|
|
535
|
+
owner: opts?.session?.owner || null,
|
|
536
|
+
role: opts?.session?.role || opts?.role || null,
|
|
537
|
+
permission: opts?.session?.permission || null,
|
|
538
|
+
toolPermission: opts?.session?.toolPermission || null,
|
|
539
|
+
profileId: opts?.session?.profileId || null,
|
|
540
|
+
sourceType: opts?.session?.sourceType || null,
|
|
541
|
+
sourceName: opts?.session?.sourceName || null,
|
|
542
|
+
sessionIdHash: opts?.sessionId ? traceHash(opts.sessionId) : null,
|
|
543
|
+
providerCacheKeyHash: opts?.providerCacheKey ? traceHash(opts.providerCacheKey) : null,
|
|
544
|
+
promptCacheKeyHash: opts?.promptCacheKey ? traceHash(opts.promptCacheKey) : null,
|
|
545
|
+
xGrokConvIdHash: provider === 'xai' && cacheRoutingKey ? traceHash(cacheRoutingKey) : null,
|
|
546
|
+
xGrokConvIdSeedHash: provider === 'xai' ? cacheRouting?.seedHash || null : null,
|
|
547
|
+
xGrokPromptPrefixHash: provider === 'xai' ? cacheRouting?.prefixHash || null : null,
|
|
548
|
+
xGrokConvIdMode: provider === 'xai' ? cacheRouting?.mode || null : null,
|
|
549
|
+
xGrokConvIdLaneIndex: provider === 'xai' ? cacheRouting?.laneIndex ?? null : null,
|
|
550
|
+
xGrokConvIdActiveLanes: provider === 'xai' ? cacheRouting?.activeLanes ?? null : null,
|
|
551
|
+
xGrokConvIdIdleLanes: provider === 'xai' ? cacheRouting?.idleLanes ?? null : null,
|
|
552
|
+
xGrokConvIdOwnerSessionHash: provider === 'xai' ? cacheRouting?.ownerSessionHash || null : null,
|
|
553
|
+
xaiReasoningEffort: provider === 'xai' ? params?.reasoning_effort || null : null,
|
|
554
|
+
messageCount: traceMessages.length,
|
|
555
|
+
messageFullHash: traceHash(stableTraceStringify(traceMessages)),
|
|
556
|
+
messagePrefixHash: traceHash(stableTraceStringify(traceMessages.slice(0, -1))),
|
|
557
|
+
lastMessageHash: traceMessages.length ? traceHash(stableTraceStringify(traceMessages.at(-1))) : null,
|
|
558
|
+
messages: summarizeTraceMessages(traceMessages),
|
|
559
|
+
toolCount: Array.isArray(rawTools) ? rawTools.length : 0,
|
|
560
|
+
toolSchemaHash: traceHash(stableTraceStringify(toolShape)),
|
|
561
|
+
usageKeys: Object.keys(usage || {}).sort(),
|
|
562
|
+
promptTokenDetailsKeys: Object.keys(usage?.prompt_tokens_details || {}).sort(),
|
|
563
|
+
inputTokenDetailsKeys: Object.keys(usage?.input_tokens_details || {}).sort(),
|
|
564
|
+
choiceMessageKeys: Object.keys(response?.choices?.[0]?.message || {}).sort(),
|
|
565
|
+
responseReasoningContent: typeof response?.choices?.[0]?.message?.reasoning_content === 'string'
|
|
566
|
+
? traceTextShape(response.choices[0].message.reasoning_content)
|
|
567
|
+
: null,
|
|
568
|
+
responseReasoningTokens: Number(usage?.completion_tokens_details?.reasoning_tokens ?? 0),
|
|
569
|
+
inputTokens,
|
|
570
|
+
outputTokens: Number(usage.completion_tokens ?? usage.output_tokens ?? 0),
|
|
571
|
+
cachedTokens,
|
|
572
|
+
cacheHitRate: inputTokens > 0 ? Number((cachedTokens / inputTokens).toFixed(6)) : null,
|
|
573
|
+
costInUsdTicks: typeof usage.cost_in_usd_ticks === 'number' ? usage.cost_in_usd_ticks : null,
|
|
574
|
+
};
|
|
575
|
+
process.stderr.write(`[compat-cache-trace] ${JSON.stringify(trace)}\n`);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
process.stderr.write(`[compat-cache-trace] failed: ${err?.message || err}\n`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function summarizeResponsesInput(input) {
|
|
582
|
+
return (input || []).map((item, index) => ({
|
|
583
|
+
index,
|
|
584
|
+
type: item?.type || null,
|
|
585
|
+
role: item?.role || null,
|
|
586
|
+
callIdHash: item?.call_id ? traceHash(item.call_id) : null,
|
|
587
|
+
name: item?.name || null,
|
|
588
|
+
content: typeof item?.content === 'string'
|
|
589
|
+
? { type: 'text', ...traceTextShape(item.content) }
|
|
590
|
+
: { type: item?.content == null ? 'null' : typeof item.content, hash: traceHash(stableTraceStringify(item?.content ?? null)) },
|
|
591
|
+
output: typeof item?.output === 'string' ? traceTextShape(item.output) : null,
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function xaiUsageStats(usage) {
|
|
596
|
+
const inputTokens = Number(usage?.input_tokens ?? usage?.prompt_tokens ?? 0);
|
|
597
|
+
const outputTokens = Number(usage?.output_tokens ?? usage?.completion_tokens ?? 0);
|
|
598
|
+
const cachedTokens = extractCompatCachedTokens(usage);
|
|
599
|
+
const hitRate = inputTokens > 0 ? Number((cachedTokens / inputTokens).toFixed(6)) : null;
|
|
600
|
+
return { inputTokens, outputTokens, cachedTokens, hitRate };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function xaiSanitizedRequestSansInput(params) {
|
|
604
|
+
const { input: _input, ...rest } = params || {};
|
|
605
|
+
const out = { ...rest };
|
|
606
|
+
if (out.prompt_cache_key) out.prompt_cache_key = traceHash(out.prompt_cache_key);
|
|
607
|
+
if (out.previous_response_id) out.previous_response_id = traceHash(out.previous_response_id);
|
|
608
|
+
if (typeof out.instructions === 'string') out.instructions = traceHash(out.instructions);
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function xaiResponsesFingerprintPayload({ model, opts, params, rawTools, response, cacheRouting, previousResponseId, inputStartIndex, transport, cacheLane }) {
|
|
613
|
+
const usage = response?.usage || {};
|
|
614
|
+
const { inputTokens, outputTokens, cachedTokens, hitRate } = xaiUsageStats(usage);
|
|
615
|
+
const toolShape = summarizeTraceTools(rawTools);
|
|
616
|
+
const instructions = typeof params?.instructions === 'string' ? params.instructions : '';
|
|
617
|
+
const requestSansInput = xaiSanitizedRequestSansInput(params);
|
|
618
|
+
const contextShape = {
|
|
619
|
+
provider: 'xai',
|
|
620
|
+
api: 'responses',
|
|
621
|
+
model: model || null,
|
|
622
|
+
promptCacheKeyHash: params?.prompt_cache_key ? traceHash(params.prompt_cache_key) : null,
|
|
623
|
+
instructions,
|
|
624
|
+
tools: toolShape,
|
|
625
|
+
reasoning: params?.reasoning || null,
|
|
626
|
+
store: params?.store ?? null,
|
|
627
|
+
};
|
|
628
|
+
const previousResponseUsed = Boolean(previousResponseId);
|
|
629
|
+
const midTurnCold = previousResponseUsed
|
|
630
|
+
&& inputTokens >= 1024
|
|
631
|
+
&& (cachedTokens <= 512 || (hitRate != null && hitRate < 0.1));
|
|
632
|
+
return {
|
|
633
|
+
provider: 'xai',
|
|
634
|
+
api: 'responses',
|
|
635
|
+
transport: transport || null,
|
|
636
|
+
model: model || null,
|
|
637
|
+
response_model: response?.model || null,
|
|
638
|
+
session_id_hash: opts?.sessionId ? traceHash(opts.sessionId) : null,
|
|
639
|
+
provider_cache_key_hash: opts?.providerCacheKey ? traceHash(opts.providerCacheKey) : null,
|
|
640
|
+
prompt_cache_key_option_hash: opts?.promptCacheKey ? traceHash(opts.promptCacheKey) : null,
|
|
641
|
+
prompt_cache_key_hash: params?.prompt_cache_key ? traceHash(params.prompt_cache_key) : null,
|
|
642
|
+
xai_prompt_prefix_hash: cacheRouting?.prefixHash || null,
|
|
643
|
+
xai_cache_mode: cacheRouting?.mode || null,
|
|
644
|
+
xai_cache_seed_hash: cacheRouting?.seedHash || null,
|
|
645
|
+
owner_session_hash: cacheRouting?.ownerSessionHash || null,
|
|
646
|
+
response_id_hash: response?.id ? traceHash(response.id) : null,
|
|
647
|
+
previous_response_id_hash: previousResponseId ? traceHash(previousResponseId) : null,
|
|
648
|
+
previous_response_used: previousResponseUsed,
|
|
649
|
+
input_start_index: inputStartIndex,
|
|
650
|
+
input_count: Array.isArray(params?.input) ? params.input.length : 0,
|
|
651
|
+
input_hash: traceHash(stableTraceStringify(params?.input || [])),
|
|
652
|
+
request_sans_input_hash: traceHash(stableTraceStringify(requestSansInput)),
|
|
653
|
+
context_prefix_hash: traceHash(stableTraceStringify(contextShape)),
|
|
654
|
+
has_instructions: instructions.length > 0,
|
|
655
|
+
instructions_chars: instructions.length,
|
|
656
|
+
instructions_hash: instructions ? traceHash(instructions) : null,
|
|
657
|
+
reasoning_effort: params?.reasoning?.effort || null,
|
|
658
|
+
tool_count: Array.isArray(rawTools) ? rawTools.length : 0,
|
|
659
|
+
tool_schema_hash: traceHash(stableTraceStringify(toolShape)),
|
|
660
|
+
tool_names_hash: traceHash(stableTraceStringify(toolShape.map(t => t?.name || null))),
|
|
661
|
+
xai_cache_lane_enabled: cacheLane?.enabled === true,
|
|
662
|
+
xai_cache_lane_hash: cacheLane?.laneKeyHash || null,
|
|
663
|
+
xai_cache_lane_shard: Number.isFinite(Number(cacheLane?.shard)) ? Number(cacheLane.shard) : null,
|
|
664
|
+
xai_cache_lane_max_in_flight: Number.isFinite(Number(cacheLane?.maxInFlight)) ? Number(cacheLane.maxInFlight) : null,
|
|
665
|
+
xai_cache_lane_wait_ms: Number.isFinite(Number(cacheLane?.waitMs)) ? Number(cacheLane.waitMs) : null,
|
|
666
|
+
xai_cache_lane_queued: cacheLane?.queued === true,
|
|
667
|
+
input_tokens: inputTokens,
|
|
668
|
+
output_tokens: outputTokens,
|
|
669
|
+
cached_tokens: cachedTokens,
|
|
670
|
+
cache_hit_rate: hitRate,
|
|
671
|
+
mid_turn_cold: midTurnCold,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function traceXaiResponsesCacheContext(args) {
|
|
676
|
+
if (!compatCacheTraceEnabled('xai')) return;
|
|
677
|
+
try {
|
|
678
|
+
const payload = xaiResponsesFingerprintPayload(args);
|
|
679
|
+
const sessionId = args?.opts?.sessionId || args?.opts?.session?.id || null;
|
|
680
|
+
const iteration = Number.isFinite(Number(args?.opts?.iteration)) ? Number(args.opts.iteration) : null;
|
|
681
|
+
appendBridgeTrace({
|
|
682
|
+
sessionId,
|
|
683
|
+
iteration,
|
|
684
|
+
kind: 'cache_context',
|
|
685
|
+
...payload,
|
|
686
|
+
payload,
|
|
687
|
+
});
|
|
688
|
+
if (payload.mid_turn_cold) {
|
|
689
|
+
const anomalyPayload = {
|
|
690
|
+
...payload,
|
|
691
|
+
anomaly: 'xai_mid_turn_cold_cache',
|
|
692
|
+
reason: 'previous_response_id_present_but_cached_tokens_low',
|
|
693
|
+
};
|
|
694
|
+
appendBridgeTrace({
|
|
695
|
+
sessionId,
|
|
696
|
+
iteration,
|
|
697
|
+
kind: 'cache_anomaly',
|
|
698
|
+
...anomalyPayload,
|
|
699
|
+
payload: anomalyPayload,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
} catch (err) {
|
|
703
|
+
process.stderr.write(`[compat-cache-trace] xai context trace failed: ${err?.message || err}\n`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function writeXaiResponsesCacheTrace({ model, opts, params, rawTools, response, cacheRouting, previousResponseId, inputStartIndex, transport, cacheLane }) {
|
|
708
|
+
if (!compatCacheTraceEnabled('xai')) return;
|
|
709
|
+
try {
|
|
710
|
+
const usage = response?.usage || {};
|
|
711
|
+
const fingerprint = xaiResponsesFingerprintPayload({
|
|
712
|
+
model,
|
|
713
|
+
opts,
|
|
714
|
+
params,
|
|
715
|
+
rawTools,
|
|
716
|
+
response,
|
|
717
|
+
cacheRouting,
|
|
718
|
+
previousResponseId,
|
|
719
|
+
inputStartIndex,
|
|
720
|
+
transport,
|
|
721
|
+
cacheLane,
|
|
722
|
+
});
|
|
723
|
+
const inputTokens = fingerprint.input_tokens;
|
|
724
|
+
const cachedTokens = fingerprint.cached_tokens;
|
|
725
|
+
const toolShape = summarizeTraceTools(rawTools);
|
|
726
|
+
const trace = {
|
|
727
|
+
event: 'responses',
|
|
728
|
+
provider: 'xai',
|
|
729
|
+
transport: transport || null,
|
|
730
|
+
model,
|
|
731
|
+
responseModel: response?.model || null,
|
|
732
|
+
responseIdHash: response?.id ? traceHash(response.id) : null,
|
|
733
|
+
previousResponseIdHash: previousResponseId ? traceHash(previousResponseId) : null,
|
|
734
|
+
owner: opts?.session?.owner || null,
|
|
735
|
+
role: opts?.session?.role || opts?.role || null,
|
|
736
|
+
permission: opts?.session?.permission || null,
|
|
737
|
+
toolPermission: opts?.session?.toolPermission || null,
|
|
738
|
+
profileId: opts?.session?.profileId || null,
|
|
739
|
+
sourceType: opts?.session?.sourceType || null,
|
|
740
|
+
sourceName: opts?.session?.sourceName || null,
|
|
741
|
+
sessionIdHash: opts?.sessionId ? traceHash(opts.sessionId) : null,
|
|
742
|
+
promptCacheKeyHash: params?.prompt_cache_key ? traceHash(params.prompt_cache_key) : null,
|
|
743
|
+
xGrokPromptPrefixHash: cacheRouting?.prefixHash || null,
|
|
744
|
+
xGrokConvIdMode: cacheRouting?.mode || null,
|
|
745
|
+
xaiReasoningEffort: params?.reasoning?.effort || null,
|
|
746
|
+
previousResponseUsed: Boolean(previousResponseId),
|
|
747
|
+
inputStartIndex,
|
|
748
|
+
inputCount: Array.isArray(params?.input) ? params.input.length : 0,
|
|
749
|
+
cacheLaneEnabled: fingerprint.xai_cache_lane_enabled,
|
|
750
|
+
cacheLaneHash: fingerprint.xai_cache_lane_hash,
|
|
751
|
+
cacheLaneShard: fingerprint.xai_cache_lane_shard,
|
|
752
|
+
cacheLaneMaxInFlight: fingerprint.xai_cache_lane_max_in_flight,
|
|
753
|
+
cacheLaneWaitMs: fingerprint.xai_cache_lane_wait_ms,
|
|
754
|
+
cacheLaneQueued: fingerprint.xai_cache_lane_queued,
|
|
755
|
+
input: summarizeResponsesInput(params?.input || []),
|
|
756
|
+
toolCount: Array.isArray(rawTools) ? rawTools.length : 0,
|
|
757
|
+
toolSchemaHash: traceHash(stableTraceStringify(toolShape)),
|
|
758
|
+
toolNamesHash: fingerprint.tool_names_hash,
|
|
759
|
+
requestSansInputHash: fingerprint.request_sans_input_hash,
|
|
760
|
+
contextPrefixHash: fingerprint.context_prefix_hash,
|
|
761
|
+
instructionsHash: fingerprint.instructions_hash,
|
|
762
|
+
instructionsChars: fingerprint.instructions_chars,
|
|
763
|
+
usageKeys: Object.keys(usage || {}).sort(),
|
|
764
|
+
inputTokenDetailsKeys: Object.keys(usage?.input_tokens_details || {}).sort(),
|
|
765
|
+
outputTokenDetailsKeys: Object.keys(usage?.output_tokens_details || {}).sort(),
|
|
766
|
+
outputTypes: (response?.output || []).map(item => item?.type || null),
|
|
767
|
+
inputTokens,
|
|
768
|
+
outputTokens: fingerprint.output_tokens,
|
|
769
|
+
cachedTokens,
|
|
770
|
+
cacheHitRate: fingerprint.cache_hit_rate,
|
|
771
|
+
midTurnCold: fingerprint.mid_turn_cold,
|
|
772
|
+
costInUsdTicks: typeof usage.cost_in_usd_ticks === 'number' ? usage.cost_in_usd_ticks : null,
|
|
773
|
+
};
|
|
774
|
+
process.stderr.write(`[compat-cache-trace] ${JSON.stringify(trace)}\n`);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
process.stderr.write(`[compat-cache-trace] failed: ${err?.message || err}\n`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function toOpenAIMessages(messages, providerName) {
|
|
781
|
+
// NOTE: chat.completions has no equivalent slot for replaying reasoning
|
|
782
|
+
// encrypted_content the way the Responses API does (no `type:'reasoning'`
|
|
783
|
+
// input item). Whatever reasoningItems may be attached to assistant
|
|
784
|
+
// messages by the openai-oauth provider is intentionally dropped here —
|
|
785
|
+
// strict providers (xai) reject unknown roles/types and would 400 the
|
|
786
|
+
// request. Documented in v0.1.160 (GPT reasoning replay).
|
|
787
|
+
//
|
|
788
|
+
// DeepSeek thinking models require the prior turn's `reasoning_content`
|
|
789
|
+
// string to be echoed back inside the assistant message, otherwise the API
|
|
790
|
+
// returns 400. xAI reasoning models also preserve their official multi-turn
|
|
791
|
+
// shape and cache prefix stability when prior assistant reasoning_content
|
|
792
|
+
// is replayed; reasoning_effort itself remains caller/user-selected.
|
|
793
|
+
const replaysReasoningContent = providerName === 'deepseek' || providerName === 'xai';
|
|
794
|
+
const out = [];
|
|
795
|
+
for (const m of messages) {
|
|
796
|
+
if (m.role === 'tool') {
|
|
797
|
+
out.push({
|
|
798
|
+
role: 'tool',
|
|
799
|
+
tool_call_id: m.toolCallId || '',
|
|
800
|
+
content: m.content,
|
|
801
|
+
});
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
805
|
+
const msg = {
|
|
806
|
+
role: 'assistant',
|
|
807
|
+
content: m.content || null,
|
|
808
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
809
|
+
id: tc.id,
|
|
810
|
+
type: 'function',
|
|
811
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
|
|
812
|
+
})),
|
|
813
|
+
};
|
|
814
|
+
if (replaysReasoningContent && m.reasoningContent) msg.reasoning_content = m.reasoningContent;
|
|
815
|
+
out.push(msg);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (m.role === 'assistant' && replaysReasoningContent && m.reasoningContent) {
|
|
819
|
+
out.push({ role: m.role, content: m.content, reasoning_content: m.reasoningContent });
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
out.push({ role: m.role, content: m.content });
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
function toOpenAITools(tools) {
|
|
827
|
+
return tools.map((t) => ({
|
|
828
|
+
type: 'function',
|
|
829
|
+
function: {
|
|
830
|
+
name: t.name,
|
|
831
|
+
description: t.description,
|
|
832
|
+
parameters: t.inputSchema,
|
|
833
|
+
},
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
function toResponsesTools(tools) {
|
|
837
|
+
return tools.map((t) => ({
|
|
838
|
+
type: 'function',
|
|
839
|
+
name: t.name,
|
|
840
|
+
description: t.description,
|
|
841
|
+
parameters: t.inputSchema,
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
function parseToolCalls(choice) {
|
|
845
|
+
const calls = choice.message?.tool_calls;
|
|
846
|
+
if (!calls?.length)
|
|
847
|
+
return undefined;
|
|
848
|
+
return calls
|
|
849
|
+
.filter((tc) => tc.type === 'function')
|
|
850
|
+
.map((tc) => ({
|
|
851
|
+
id: tc.id,
|
|
852
|
+
name: tc.function.name,
|
|
853
|
+
arguments: JSON.parse(tc.function.arguments || '{}'),
|
|
854
|
+
}));
|
|
855
|
+
}
|
|
856
|
+
function parseJsonObject(value) {
|
|
857
|
+
try {
|
|
858
|
+
const parsed = JSON.parse(value || '{}');
|
|
859
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
860
|
+
} catch {
|
|
861
|
+
return {};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function parseResponsesToolCalls(response) {
|
|
865
|
+
const out = [];
|
|
866
|
+
for (const item of response?.output || []) {
|
|
867
|
+
if (item?.type !== 'function_call') continue;
|
|
868
|
+
out.push({
|
|
869
|
+
id: item.call_id || item.id,
|
|
870
|
+
name: item.name,
|
|
871
|
+
arguments: parseJsonObject(item.arguments),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return out.length ? out : undefined;
|
|
875
|
+
}
|
|
876
|
+
function responseOutputText(response) {
|
|
877
|
+
if (typeof response?.output_text === 'string') return response.output_text;
|
|
878
|
+
const chunks = [];
|
|
879
|
+
for (const item of response?.output || []) {
|
|
880
|
+
if (item?.type !== 'message' || !Array.isArray(item.content)) continue;
|
|
881
|
+
for (const part of item.content) {
|
|
882
|
+
if (part?.type === 'output_text' && typeof part.text === 'string') chunks.push(part.text);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return chunks.join('');
|
|
886
|
+
}
|
|
887
|
+
function toResponsesInputMessage(m) {
|
|
888
|
+
if (m.role === 'tool') {
|
|
889
|
+
return {
|
|
890
|
+
type: 'function_call_output',
|
|
891
|
+
call_id: m.toolCallId || '',
|
|
892
|
+
output: typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
896
|
+
const items = [];
|
|
897
|
+
if (m.content) items.push({ role: 'assistant', content: m.content });
|
|
898
|
+
for (const tc of m.toolCalls) {
|
|
899
|
+
items.push({
|
|
900
|
+
type: 'function_call',
|
|
901
|
+
call_id: tc.id,
|
|
902
|
+
name: tc.name,
|
|
903
|
+
arguments: JSON.stringify(tc.arguments || {}),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return items;
|
|
907
|
+
}
|
|
908
|
+
return { role: m.role, content: m.content || '' };
|
|
909
|
+
}
|
|
910
|
+
function xaiSystemInstructions(messages) {
|
|
911
|
+
const instructions = (messages || [])
|
|
912
|
+
.filter(m => m?.role === 'system')
|
|
913
|
+
.map(m => String(m.content || ''))
|
|
914
|
+
.filter(Boolean)
|
|
915
|
+
.join('\n\n');
|
|
916
|
+
return instructions || undefined;
|
|
917
|
+
}
|
|
918
|
+
function toXaiResponsesInput(messages, providerState, options = {}) {
|
|
919
|
+
const includeSystem = options.includeSystem !== false;
|
|
920
|
+
const state = providerState?.xaiResponses || null;
|
|
921
|
+
let startIndex = 0;
|
|
922
|
+
const previousResponseId = typeof state?.previousResponseId === 'string' ? state.previousResponseId : null;
|
|
923
|
+
if (previousResponseId) {
|
|
924
|
+
const seen = Number.isInteger(state?.seenMessageCount) ? state.seenMessageCount : messages.length;
|
|
925
|
+
startIndex = Math.max(0, Math.min(seen, messages.length));
|
|
926
|
+
if (messages[startIndex]?.role === 'assistant') startIndex += 1;
|
|
927
|
+
}
|
|
928
|
+
const input = [];
|
|
929
|
+
for (const m of messages.slice(startIndex)) {
|
|
930
|
+
if (!includeSystem && m.role === 'system') continue;
|
|
931
|
+
const converted = toResponsesInputMessage(m);
|
|
932
|
+
if (Array.isArray(converted)) input.push(...converted);
|
|
933
|
+
else input.push(converted);
|
|
934
|
+
}
|
|
935
|
+
return { input, previousResponseId, startIndex };
|
|
936
|
+
}
|
|
937
|
+
export class OpenAICompatProvider {
|
|
938
|
+
// Chat Completions prompt_tokens is already the total (includes cached).
|
|
939
|
+
// Covers grok-oauth and all OPENAI_COMPAT_PRESETS. See registry.mjs.
|
|
940
|
+
static inputExcludesCache = false;
|
|
941
|
+
name;
|
|
942
|
+
client;
|
|
943
|
+
defaultModel;
|
|
944
|
+
config;
|
|
945
|
+
baseURL;
|
|
946
|
+
apiKey;
|
|
947
|
+
defaultHeaders;
|
|
948
|
+
constructor(name, config) {
|
|
949
|
+
const preset = PRESETS[name];
|
|
950
|
+
const baseURL = assertSafeBaseURL(config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', name);
|
|
951
|
+
const apiKey = config.apiKey || 'no-key';
|
|
952
|
+
this.name = name;
|
|
953
|
+
this.config = config;
|
|
954
|
+
this.baseURL = baseURL;
|
|
955
|
+
this.apiKey = apiKey;
|
|
956
|
+
// Merge caller-supplied headers (config.extraHeaders) over the preset's.
|
|
957
|
+
// Used e.g. by grok-oauth to inject the Grok CLI client headers for the
|
|
958
|
+
// grok-build proxy. Backward-compatible: providers that pass no
|
|
959
|
+
// extraHeaders behave exactly as before.
|
|
960
|
+
this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(config.extraHeaders || {}) };
|
|
961
|
+
this.defaultModel = preset?.defaultModel || 'default';
|
|
962
|
+
this.client = new OpenAI({
|
|
963
|
+
baseURL,
|
|
964
|
+
apiKey,
|
|
965
|
+
defaultHeaders: this.defaultHeaders,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
reloadApiKey() {
|
|
969
|
+
try {
|
|
970
|
+
const freshConfig = loadConfig();
|
|
971
|
+
const cfg = freshConfig.providers?.[this.name];
|
|
972
|
+
const preset = PRESETS[this.name];
|
|
973
|
+
const newKey = cfg?.apiKey || this.config.apiKey;
|
|
974
|
+
const baseURL = assertSafeBaseURL(cfg?.baseURL || this.config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', this.name);
|
|
975
|
+
if (newKey) {
|
|
976
|
+
this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey, baseURL };
|
|
977
|
+
this.baseURL = baseURL;
|
|
978
|
+
this.apiKey = newKey;
|
|
979
|
+
this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(this.config.extraHeaders || {}) };
|
|
980
|
+
this.client = new OpenAI({
|
|
981
|
+
baseURL,
|
|
982
|
+
apiKey: newKey,
|
|
983
|
+
defaultHeaders: this.defaultHeaders,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
} catch { /* best effort */ }
|
|
987
|
+
}
|
|
988
|
+
async send(messages, model, tools, sendOpts) {
|
|
989
|
+
try {
|
|
990
|
+
return await this._doSend(messages, model, tools, sendOpts);
|
|
991
|
+
} catch (err) {
|
|
992
|
+
if (err.message && (err.message.includes('401') || err.message.includes('403'))) {
|
|
993
|
+
process.stderr.write(`[provider] Auth error, re-reading config...\n`);
|
|
994
|
+
this.reloadApiKey();
|
|
995
|
+
return await this._doSend(messages, model, tools, sendOpts);
|
|
996
|
+
}
|
|
997
|
+
throw err;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async _doSend(messages, model, tools, sendOpts) {
|
|
1001
|
+
const useModel = model || this.defaultModel;
|
|
1002
|
+
const opts = sendOpts || {};
|
|
1003
|
+
if (this.name === 'xai' && useXaiResponsesApi(opts, this.config)) {
|
|
1004
|
+
if (useXaiResponsesWebSocket(opts, this.config)) {
|
|
1005
|
+
return await this._doSendXaiResponsesWebSocket(messages, useModel, tools, opts);
|
|
1006
|
+
}
|
|
1007
|
+
return await this._doSendXaiResponses(messages, useModel, tools, opts);
|
|
1008
|
+
}
|
|
1009
|
+
const signal = opts.signal || null;
|
|
1010
|
+
if (signal?.aborted) {
|
|
1011
|
+
const reason = signal.reason;
|
|
1012
|
+
throw reason instanceof Error ? reason : new Error('OpenAI-compat request aborted by session close');
|
|
1013
|
+
}
|
|
1014
|
+
const params = {
|
|
1015
|
+
model: useModel,
|
|
1016
|
+
messages: toOpenAIMessages(messages, this.name),
|
|
1017
|
+
};
|
|
1018
|
+
if (tools?.length) {
|
|
1019
|
+
params.tools = toOpenAITools(tools);
|
|
1020
|
+
}
|
|
1021
|
+
if (this.name === 'xai') {
|
|
1022
|
+
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
1023
|
+
?? opts.effort
|
|
1024
|
+
?? this.config?.reasoningEffort
|
|
1025
|
+
?? process.env.MIXDOG_XAI_REASONING_EFFORT);
|
|
1026
|
+
if (reasoningEffort) params.reasoning_effort = reasoningEffort;
|
|
1027
|
+
}
|
|
1028
|
+
const totalSignal = createTimeoutSignal(signal, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, `${this.name} total`);
|
|
1029
|
+
const cacheRouting = this.name === 'xai'
|
|
1030
|
+
? xaiCacheRouting(opts, params, tools || [], useModel)
|
|
1031
|
+
: null;
|
|
1032
|
+
const cacheRoutingKey = cacheRouting?.key || null;
|
|
1033
|
+
// Note: x-grok-conv-id is documented as a routing hint, but in our
|
|
1034
|
+
// measured parallel-worker traffic it caused alternating cold caches
|
|
1035
|
+
// (server-side per-conv shard isolation). Vercel ai-sdk and other
|
|
1036
|
+
// reference clients omit it entirely and rely on xAI's automatic
|
|
1037
|
+
// prompt-prefix caching, which holds up to 95%+ hit even across
|
|
1038
|
+
// parallel workers. Keep the header off by default.
|
|
1039
|
+
// Shared retry: deepseek / xai / other compat backends all sit behind
|
|
1040
|
+
// their own load balancers and emit 5xx / "overloaded" under burst
|
|
1041
|
+
// traffic. The withRetry wrapper preserves abort behavior via
|
|
1042
|
+
// mergedSignal and only retries when classifyError() says transient.
|
|
1043
|
+
let response;
|
|
1044
|
+
try {
|
|
1045
|
+
response = await withRetry(
|
|
1046
|
+
({ signal: attemptSignal }) => this.client.chat.completions.create(params, { signal: attemptSignal }),
|
|
1047
|
+
{
|
|
1048
|
+
signal: totalSignal.signal,
|
|
1049
|
+
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1050
|
+
perAttemptLabel: `${this.name} first byte`,
|
|
1051
|
+
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
1052
|
+
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
1053
|
+
process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
);
|
|
1057
|
+
} finally {
|
|
1058
|
+
totalSignal.cleanup();
|
|
1059
|
+
}
|
|
1060
|
+
const choice = response.choices[0];
|
|
1061
|
+
const toolCalls = choice ? parseToolCalls(choice) : undefined;
|
|
1062
|
+
// Capture finish_reason early so we can refuse to return an
|
|
1063
|
+
// incomplete completion as final content. OpenAI-compat backends use
|
|
1064
|
+
// `length` (max_tokens / model context overflow) and `content_filter`
|
|
1065
|
+
// (moderation cutoff) to flag responses that were terminated before
|
|
1066
|
+
// the model finished its turn — treating those as success silently
|
|
1067
|
+
// surfaces truncated text and lets the loop accept a partial answer.
|
|
1068
|
+
const stopReason = choice?.finish_reason || null;
|
|
1069
|
+
if (stopReason === 'length' || stopReason === 'content_filter') {
|
|
1070
|
+
const err = Object.assign(
|
|
1071
|
+
new Error(`${this.name} response incomplete: finish_reason=${stopReason}`),
|
|
1072
|
+
{
|
|
1073
|
+
name: 'ProviderIncompleteError',
|
|
1074
|
+
code: 'PROVIDER_INCOMPLETE',
|
|
1075
|
+
providerIncomplete: true,
|
|
1076
|
+
finishReason: stopReason,
|
|
1077
|
+
partialContent: choice?.message?.content || '',
|
|
1078
|
+
partialToolCalls: toolCalls,
|
|
1079
|
+
model: response.model || useModel,
|
|
1080
|
+
responseId: response.id || null,
|
|
1081
|
+
rawUsage: response.usage || null,
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
throw err;
|
|
1085
|
+
}
|
|
1086
|
+
writeCompatCacheTrace({
|
|
1087
|
+
provider: this.name,
|
|
1088
|
+
model: useModel,
|
|
1089
|
+
opts,
|
|
1090
|
+
params,
|
|
1091
|
+
rawTools: tools || [],
|
|
1092
|
+
response,
|
|
1093
|
+
cacheRoutingKey,
|
|
1094
|
+
cacheRouting,
|
|
1095
|
+
});
|
|
1096
|
+
if (response.usage) {
|
|
1097
|
+
const inputTokens = Number(response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0);
|
|
1098
|
+
const cachedTokens = extractCompatCachedTokens(response.usage);
|
|
1099
|
+
traceBridgeUsage({
|
|
1100
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
1101
|
+
iteration: Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null,
|
|
1102
|
+
inputTokens,
|
|
1103
|
+
outputTokens: Number(response.usage.completion_tokens ?? response.usage.output_tokens ?? 0),
|
|
1104
|
+
cachedTokens,
|
|
1105
|
+
cacheWriteTokens: 0,
|
|
1106
|
+
promptTokens: inputTokens,
|
|
1107
|
+
model: response.model || useModel,
|
|
1108
|
+
modelDisplay: response.model || useModel,
|
|
1109
|
+
responseId: response.id || null,
|
|
1110
|
+
rawUsage: response.usage,
|
|
1111
|
+
provider: this.name,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
// Capture provider reasoning_content so loop.mjs can attach it to the
|
|
1115
|
+
// assistant message and echo it back next turn for providers that
|
|
1116
|
+
// require or benefit from that official multi-turn shape.
|
|
1117
|
+
const capturesReasoningContent = this.name === 'deepseek' || this.name === 'xai';
|
|
1118
|
+
const reasoningContent = (capturesReasoningContent && typeof choice?.message?.reasoning_content === 'string')
|
|
1119
|
+
? choice.message.reasoning_content
|
|
1120
|
+
: null;
|
|
1121
|
+
return {
|
|
1122
|
+
content: choice?.message?.content || '',
|
|
1123
|
+
model: response.model,
|
|
1124
|
+
toolCalls,
|
|
1125
|
+
stopReason,
|
|
1126
|
+
...(reasoningContent ? { reasoningContent } : {}),
|
|
1127
|
+
usage: response.usage ? (() => {
|
|
1128
|
+
const input = response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0;
|
|
1129
|
+
const cached = extractCompatCachedTokens(response.usage);
|
|
1130
|
+
// xAI Grok returns the actual billed amount in `cost_in_usd_ticks`
|
|
1131
|
+
// (1 tick = $1e-10, per docs.x.ai). Surface it as costUsd so the
|
|
1132
|
+
// session manager skips the catalog-rate fallback and records the
|
|
1133
|
+
// provider-billed value verbatim.
|
|
1134
|
+
const ticks = response.usage.cost_in_usd_ticks;
|
|
1135
|
+
const costUsd = typeof ticks === 'number' && ticks >= 0
|
|
1136
|
+
? Number((ticks * 1e-10).toFixed(8))
|
|
1137
|
+
: undefined;
|
|
1138
|
+
return {
|
|
1139
|
+
inputTokens: input,
|
|
1140
|
+
outputTokens: response.usage.completion_tokens ?? response.usage.output_tokens ?? 0,
|
|
1141
|
+
cachedTokens: cached,
|
|
1142
|
+
// Chat Completions prompt_tokens is already the total prompt
|
|
1143
|
+
// the model ingested (cached is a subset) — alias directly.
|
|
1144
|
+
promptTokens: input,
|
|
1145
|
+
raw: { ...response.usage },
|
|
1146
|
+
...(costUsd != null ? { costUsd } : {}),
|
|
1147
|
+
};
|
|
1148
|
+
})() : undefined,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
async _doSendXaiResponses(messages, useModel, tools, opts) {
|
|
1152
|
+
const signal = opts.signal || null;
|
|
1153
|
+
if (signal?.aborted) {
|
|
1154
|
+
const reason = signal.reason;
|
|
1155
|
+
throw reason instanceof Error ? reason : new Error('xAI Responses request aborted by session close');
|
|
1156
|
+
}
|
|
1157
|
+
const chatMessagesForTrace = toOpenAIMessages(messages, this.name);
|
|
1158
|
+
const cacheRouting = xaiResponsesCacheRouting(opts, { messages: chatMessagesForTrace }, tools || [], useModel);
|
|
1159
|
+
const { input, previousResponseId, startIndex } = toXaiResponsesInput(messages, opts.providerState);
|
|
1160
|
+
const params = {
|
|
1161
|
+
model: useModel,
|
|
1162
|
+
input,
|
|
1163
|
+
store: true,
|
|
1164
|
+
prompt_cache_key: cacheRouting.key,
|
|
1165
|
+
};
|
|
1166
|
+
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
1167
|
+
if (tools?.length) params.tools = toResponsesTools(tools);
|
|
1168
|
+
// Non-streaming transport: there are no deltas to report, so without
|
|
1169
|
+
// an explicit stage the session sits on the loop's per-iteration
|
|
1170
|
+
// 'connecting' reset for the whole generation (bridge list shows a
|
|
1171
|
+
// working session as stuck). Report 'requesting' for the in-flight
|
|
1172
|
+
// window and fire one delta on arrival to feed the stall watchdog.
|
|
1173
|
+
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
1174
|
+
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
1175
|
+
?? opts.effort
|
|
1176
|
+
?? this.config?.reasoningEffort
|
|
1177
|
+
?? process.env.MIXDOG_XAI_REASONING_EFFORT);
|
|
1178
|
+
if (reasoningEffort) params.reasoning = { effort: reasoningEffort };
|
|
1179
|
+
let response;
|
|
1180
|
+
let cacheLane = null;
|
|
1181
|
+
const scheduled = await withXaiResponsesCacheLane({
|
|
1182
|
+
opts,
|
|
1183
|
+
config: this.config,
|
|
1184
|
+
cacheRouting,
|
|
1185
|
+
model: useModel,
|
|
1186
|
+
transport: 'http',
|
|
1187
|
+
previousResponseId,
|
|
1188
|
+
inputCount: Array.isArray(input) ? input.length : 0,
|
|
1189
|
+
signal,
|
|
1190
|
+
}, async (laneMeta) => {
|
|
1191
|
+
cacheLane = laneMeta;
|
|
1192
|
+
const totalSignal = createTimeoutSignal(signal, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, 'xai responses total');
|
|
1193
|
+
try {
|
|
1194
|
+
return await withRetry(
|
|
1195
|
+
({ signal: attemptSignal }) => this.client.responses.create(params, { signal: attemptSignal }),
|
|
1196
|
+
{
|
|
1197
|
+
signal: totalSignal.signal,
|
|
1198
|
+
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1199
|
+
perAttemptLabel: 'xai responses first byte',
|
|
1200
|
+
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
1201
|
+
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
1202
|
+
process.stderr.write(`[xai:responses] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
);
|
|
1206
|
+
} finally {
|
|
1207
|
+
totalSignal.cleanup();
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
response = scheduled.value;
|
|
1211
|
+
cacheLane = cacheLane || scheduled.laneMeta;
|
|
1212
|
+
try { opts.onStreamDelta?.(); } catch { /* heartbeat best-effort */ }
|
|
1213
|
+
const toolCalls = parseResponsesToolCalls(response);
|
|
1214
|
+
writeXaiResponsesCacheTrace({
|
|
1215
|
+
model: useModel,
|
|
1216
|
+
opts,
|
|
1217
|
+
params,
|
|
1218
|
+
rawTools: tools || [],
|
|
1219
|
+
response,
|
|
1220
|
+
cacheRouting,
|
|
1221
|
+
previousResponseId,
|
|
1222
|
+
inputStartIndex: startIndex,
|
|
1223
|
+
transport: 'http',
|
|
1224
|
+
cacheLane,
|
|
1225
|
+
});
|
|
1226
|
+
traceXaiResponsesCacheContext({
|
|
1227
|
+
model: useModel,
|
|
1228
|
+
opts,
|
|
1229
|
+
params,
|
|
1230
|
+
rawTools: tools || [],
|
|
1231
|
+
response,
|
|
1232
|
+
cacheRouting,
|
|
1233
|
+
previousResponseId,
|
|
1234
|
+
inputStartIndex: startIndex,
|
|
1235
|
+
transport: 'http',
|
|
1236
|
+
cacheLane,
|
|
1237
|
+
});
|
|
1238
|
+
if (response.usage) {
|
|
1239
|
+
const inputTokens = Number(response.usage.input_tokens ?? response.usage.prompt_tokens ?? 0);
|
|
1240
|
+
const cachedTokens = extractCompatCachedTokens(response.usage);
|
|
1241
|
+
traceBridgeUsage({
|
|
1242
|
+
sessionId: opts.sessionId || opts.session?.id || null,
|
|
1243
|
+
iteration: Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null,
|
|
1244
|
+
inputTokens,
|
|
1245
|
+
outputTokens: Number(response.usage.output_tokens ?? response.usage.completion_tokens ?? 0),
|
|
1246
|
+
cachedTokens,
|
|
1247
|
+
cacheWriteTokens: 0,
|
|
1248
|
+
promptTokens: inputTokens,
|
|
1249
|
+
model: response.model || useModel,
|
|
1250
|
+
modelDisplay: response.model || useModel,
|
|
1251
|
+
responseId: response.id || null,
|
|
1252
|
+
rawUsage: response.usage,
|
|
1253
|
+
provider: 'xai',
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
content: responseOutputText(response),
|
|
1258
|
+
model: response.model || useModel,
|
|
1259
|
+
toolCalls,
|
|
1260
|
+
providerState: {
|
|
1261
|
+
...(opts.providerState || {}),
|
|
1262
|
+
xaiResponses: {
|
|
1263
|
+
previousResponseId: response.id,
|
|
1264
|
+
seenMessageCount: Array.isArray(messages) ? messages.length : 0,
|
|
1265
|
+
model: response.model || useModel,
|
|
1266
|
+
updatedAt: Date.now(),
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
usage: response.usage ? (() => {
|
|
1270
|
+
const inputTokens = response.usage.input_tokens ?? response.usage.prompt_tokens ?? 0;
|
|
1271
|
+
const ticks = response.usage.cost_in_usd_ticks;
|
|
1272
|
+
const costUsd = typeof ticks === 'number' && ticks >= 0
|
|
1273
|
+
? Number((ticks * 1e-10).toFixed(8))
|
|
1274
|
+
: undefined;
|
|
1275
|
+
return {
|
|
1276
|
+
inputTokens,
|
|
1277
|
+
outputTokens: response.usage.output_tokens ?? response.usage.completion_tokens ?? 0,
|
|
1278
|
+
cachedTokens: extractCompatCachedTokens(response.usage),
|
|
1279
|
+
promptTokens: inputTokens,
|
|
1280
|
+
raw: { ...response.usage },
|
|
1281
|
+
...(costUsd != null ? { costUsd } : {}),
|
|
1282
|
+
};
|
|
1283
|
+
})() : undefined,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
async _doSendXaiResponsesWebSocket(messages, useModel, tools, opts) {
|
|
1287
|
+
const signal = opts.signal || null;
|
|
1288
|
+
if (signal?.aborted) {
|
|
1289
|
+
const reason = signal.reason;
|
|
1290
|
+
throw reason instanceof Error ? reason : new Error('xAI Responses WebSocket request aborted by session close');
|
|
1291
|
+
}
|
|
1292
|
+
const apiKey = this.config?.apiKey || process.env.XAI_API_KEY;
|
|
1293
|
+
if (!apiKey) throw new Error('xAI API key not configured');
|
|
1294
|
+
const chatMessagesForTrace = toOpenAIMessages(messages, this.name);
|
|
1295
|
+
const cacheRouting = xaiResponsesCacheRouting(opts, { messages: chatMessagesForTrace }, tools || [], useModel);
|
|
1296
|
+
const { input, previousResponseId, startIndex } = toXaiResponsesInput(messages, opts.providerState, { includeSystem: false });
|
|
1297
|
+
const params = {
|
|
1298
|
+
model: useModel,
|
|
1299
|
+
input,
|
|
1300
|
+
// xAI's WebSocket continuation is documented for store=false, but
|
|
1301
|
+
// the public endpoint currently returns previous_response_not_found
|
|
1302
|
+
// in our live probes unless the chain is stored.
|
|
1303
|
+
store: true,
|
|
1304
|
+
prompt_cache_key: cacheRouting.key,
|
|
1305
|
+
};
|
|
1306
|
+
const instructions = xaiSystemInstructions(messages);
|
|
1307
|
+
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
1308
|
+
// xAI rejects instructions together with previous_response_id; the
|
|
1309
|
+
// first response already anchors instructions for the continuation.
|
|
1310
|
+
else if (instructions) params.instructions = instructions;
|
|
1311
|
+
if (tools?.length) params.tools = toResponsesTools(tools);
|
|
1312
|
+
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
1313
|
+
?? opts.effort
|
|
1314
|
+
?? this.config?.reasoningEffort
|
|
1315
|
+
?? process.env.MIXDOG_XAI_REASONING_EFFORT);
|
|
1316
|
+
if (reasoningEffort) params.reasoning = { effort: reasoningEffort };
|
|
1317
|
+
const warmupBody = useXaiResponsesWebSocketWarmup(opts, this.config, {
|
|
1318
|
+
previousResponseId,
|
|
1319
|
+
instructions,
|
|
1320
|
+
rawTools: tools || [],
|
|
1321
|
+
})
|
|
1322
|
+
? { ...params, generate: false, input: [] }
|
|
1323
|
+
: null;
|
|
1324
|
+
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
1325
|
+
let cacheLane = null;
|
|
1326
|
+
const scheduled = await withXaiResponsesCacheLane({
|
|
1327
|
+
opts,
|
|
1328
|
+
config: this.config,
|
|
1329
|
+
cacheRouting,
|
|
1330
|
+
model: useModel,
|
|
1331
|
+
transport: 'websocket',
|
|
1332
|
+
previousResponseId,
|
|
1333
|
+
inputCount: Array.isArray(input) ? input.length : 0,
|
|
1334
|
+
signal,
|
|
1335
|
+
}, async (laneMeta) => {
|
|
1336
|
+
cacheLane = laneMeta;
|
|
1337
|
+
return await sendViaWebSocket({
|
|
1338
|
+
auth: { type: 'xai', apiKey },
|
|
1339
|
+
body: params,
|
|
1340
|
+
sendOpts: opts,
|
|
1341
|
+
onStreamDelta: typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null,
|
|
1342
|
+
onToolCall: typeof opts.onToolCall === 'function' ? opts.onToolCall : null,
|
|
1343
|
+
onStageChange: typeof opts.onStageChange === 'function' ? opts.onStageChange : null,
|
|
1344
|
+
externalSignal: signal,
|
|
1345
|
+
poolKey: opts.sessionId || opts.session?.id || null,
|
|
1346
|
+
cacheKey: cacheRouting.key,
|
|
1347
|
+
iteration,
|
|
1348
|
+
useModel,
|
|
1349
|
+
displayModel: (id) => id,
|
|
1350
|
+
includeResponseId: true,
|
|
1351
|
+
traceProvider: 'xai',
|
|
1352
|
+
logSuppressedReasoningDeltas: false,
|
|
1353
|
+
warmupBody,
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
const result = scheduled.value;
|
|
1357
|
+
cacheLane = cacheLane || scheduled.laneMeta;
|
|
1358
|
+
const responseId = result.responseId || previousResponseId || null;
|
|
1359
|
+
const rawUsage = result.usage?.raw || result.usage || null;
|
|
1360
|
+
const traceParams = result.__warmup?.requestBody || params;
|
|
1361
|
+
writeXaiResponsesCacheTrace({
|
|
1362
|
+
model: useModel,
|
|
1363
|
+
opts,
|
|
1364
|
+
params: traceParams,
|
|
1365
|
+
rawTools: tools || [],
|
|
1366
|
+
response: {
|
|
1367
|
+
id: responseId,
|
|
1368
|
+
model: result.model || useModel,
|
|
1369
|
+
output: [],
|
|
1370
|
+
usage: rawUsage,
|
|
1371
|
+
},
|
|
1372
|
+
cacheRouting,
|
|
1373
|
+
previousResponseId,
|
|
1374
|
+
inputStartIndex: startIndex,
|
|
1375
|
+
transport: 'websocket',
|
|
1376
|
+
cacheLane,
|
|
1377
|
+
});
|
|
1378
|
+
traceXaiResponsesCacheContext({
|
|
1379
|
+
model: useModel,
|
|
1380
|
+
opts,
|
|
1381
|
+
params: traceParams,
|
|
1382
|
+
rawTools: tools || [],
|
|
1383
|
+
response: {
|
|
1384
|
+
id: responseId,
|
|
1385
|
+
model: result.model || useModel,
|
|
1386
|
+
output: [],
|
|
1387
|
+
usage: rawUsage,
|
|
1388
|
+
},
|
|
1389
|
+
cacheRouting,
|
|
1390
|
+
previousResponseId,
|
|
1391
|
+
inputStartIndex: startIndex,
|
|
1392
|
+
transport: 'websocket',
|
|
1393
|
+
cacheLane,
|
|
1394
|
+
});
|
|
1395
|
+
const ticks = rawUsage?.cost_in_usd_ticks;
|
|
1396
|
+
const costUsd = typeof ticks === 'number' && ticks >= 0
|
|
1397
|
+
? Number((ticks * 1e-10).toFixed(8))
|
|
1398
|
+
: undefined;
|
|
1399
|
+
return {
|
|
1400
|
+
content: result.content || '',
|
|
1401
|
+
model: result.model || useModel,
|
|
1402
|
+
toolCalls: result.toolCalls,
|
|
1403
|
+
providerState: {
|
|
1404
|
+
...(opts.providerState || {}),
|
|
1405
|
+
xaiResponses: {
|
|
1406
|
+
previousResponseId: responseId,
|
|
1407
|
+
seenMessageCount: Array.isArray(messages) ? messages.length : 0,
|
|
1408
|
+
model: result.model || useModel,
|
|
1409
|
+
updatedAt: Date.now(),
|
|
1410
|
+
transport: 'websocket',
|
|
1411
|
+
},
|
|
1412
|
+
},
|
|
1413
|
+
usage: result.usage ? {
|
|
1414
|
+
...result.usage,
|
|
1415
|
+
...(costUsd != null ? { costUsd } : {}),
|
|
1416
|
+
} : undefined,
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
async _fetchModelItems() {
|
|
1420
|
+
const timeout = createTimeoutSignal(null, MODEL_LIST_TIMEOUT_MS, `${this.name} model list`);
|
|
1421
|
+
try {
|
|
1422
|
+
const res = await fetch(`${String(this.baseURL || '').replace(/\/+$/, '')}/models`, {
|
|
1423
|
+
method: 'GET',
|
|
1424
|
+
headers: {
|
|
1425
|
+
Authorization: `Bearer ${this.apiKey || 'no-key'}`,
|
|
1426
|
+
...(this.defaultHeaders || {}),
|
|
1427
|
+
},
|
|
1428
|
+
signal: timeout.signal,
|
|
1429
|
+
});
|
|
1430
|
+
if (!res.ok) throw new Error(`${this.name} models ${res.status}`);
|
|
1431
|
+
const data = await res.json();
|
|
1432
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
1433
|
+
if (Array.isArray(data)) return data;
|
|
1434
|
+
return [];
|
|
1435
|
+
} finally {
|
|
1436
|
+
timeout.cleanup();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
async listModels() {
|
|
1440
|
+
try {
|
|
1441
|
+
const list = await this._fetchModelItems();
|
|
1442
|
+
const models = [];
|
|
1443
|
+
for (const m of list) {
|
|
1444
|
+
models.push({
|
|
1445
|
+
id: m?.id,
|
|
1446
|
+
name: m?.id,
|
|
1447
|
+
provider: this.name,
|
|
1448
|
+
contextWindow: 0,
|
|
1449
|
+
created: typeof m?.created === 'number' ? m.created : null,
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
return models.filter(m => m.id);
|
|
1453
|
+
}
|
|
1454
|
+
catch {
|
|
1455
|
+
return [];
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async isAvailable() {
|
|
1459
|
+
try {
|
|
1460
|
+
await this._fetchModelItems();
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
catch {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|