mixdog 1.2.5 → 1.2.6
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/package.json +1 -1
- package/scripts/lib/gate-scheduler.mjs +2 -2
- package/scripts/lib/run-node-tests.mjs +5 -14
- package/scripts/provider-pingpong-live.mjs +9 -9
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +2 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/browser-use/SKILL.md +2 -1
- package/src/defaults/skills/computer-use/SKILL.md +3 -1
- package/src/defaults/skills/visualize/SKILL.md +3 -2
- package/src/lib/rules-builder.cjs +1 -1
- package/src/output-styles/common.md +22 -6
- package/src/output-styles/detailed.md +7 -5
- package/src/output-styles/minimal.md +7 -3
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/AGENT.md +3 -1
- package/src/rules/lead/LEAD.md +4 -3
- package/src/rules/shared/10-tool-workflow.md +2 -1
- package/src/rules/shared/60-verification.md +4 -5
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
- package/src/runtime/agent/orchestrator/config.mjs +6 -0
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
- package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
- package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
- package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
- package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
- package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
- package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
- package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
- package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/attachments/office-extract.mjs +11 -9
- package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
- package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
- package/src/runtime/office/core/tabular.mjs +1 -1
- package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
- package/src/runtime/office/portable/docx-formatting.mjs +1 -1
- package/src/runtime/office/portable/docx-runs.mjs +1 -1
- package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
- package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
- package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
- package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
- package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
- package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
- package/src/runtime/office/portable/portable-opc.mjs +27 -13
- package/src/runtime/office/portable/portable-pivot.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
- package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
- package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
- package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
- package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
- package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
- package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
- package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
- package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
- package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
- package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
- package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
- package/src/runtime/office/portable/portable-xml.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +22 -13
- package/src/runtime/shared/background-tasks.mjs +60 -20
- package/src/runtime/shared/child-spawn-gate.mjs +26 -16
- package/src/runtime/shared/config.mjs +0 -10
- package/src/runtime/shared/file-lock.mjs +7 -1
- package/src/runtime/shared/file-permissions-worker.mjs +38 -0
- package/src/runtime/shared/file-permissions.d.mts +19 -4
- package/src/runtime/shared/file-permissions.mjs +310 -4
- package/src/runtime/shared/git-runner-worker.mjs +6 -78
- package/src/runtime/shared/llm/http-agent.mjs +7 -3
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
- package/src/runtime/shared/memory-snapshot.mjs +5 -1
- package/src/runtime/shared/notify-trace.mjs +8 -1
- package/src/runtime/shared/pristine-execution-contract.json +5 -1
- package/src/runtime/shared/process-runner-worker.mjs +7 -0
- package/src/runtime/shared/process-runner.mjs +38 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -1
- package/src/runtime/shared/provider-api-key.mjs +0 -1
- package/src/runtime/shared/read-row-numbers.mjs +1 -1
- package/src/runtime/shared/resource-admission.mjs +60 -18
- package/src/runtime/shared/run-child.mjs +84 -0
- package/src/runtime/shared/session-activity-signal.mjs +23 -0
- package/src/runtime/shared/shell-output-summary.mjs +23 -0
- package/src/runtime/shared/singleton-owner.mjs +126 -42
- package/src/runtime/shared/tool-output-limit.mjs +4 -2
- package/src/runtime/shared/tool-output-marker.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +28 -9
- package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
- package/src/runtime/shared/worktrees/git-head.mjs +20 -2
- package/src/runtime/shared/worktrees/lock.mjs +3 -2
- package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
- package/src/runtime/tidy/runners/biome.mjs +3 -1
- package/src/runtime/tidy/shadow.mjs +2 -1
- package/src/runtime/web-search/index.mjs +5 -7
- package/src/runtime/web-search/lib/formatter.mjs +42 -6
- package/src/runtime/web-search/tool-defs.mjs +6 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
- package/src/session-runtime/lifecycle/shared.mjs +19 -2
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +5 -2
- package/src/session-runtime/lifecycle-api.mjs +3 -1
- package/src/session-runtime/provider-auth/login.mjs +10 -6
- package/src/session-runtime/provider-auth/setup.mjs +19 -2
- package/src/session-runtime/provider-catalog-cache.mjs +101 -25
- package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
- package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
- package/src/session-runtime/provider-models.mjs +76 -10
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
- package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
- package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
- package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
- package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
- package/src/session-runtime/services/agent-tool.mjs +20 -0
- package/src/session-runtime/services/provider-admin.mjs +92 -20
- package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
- package/src/session-runtime/services/usage-dashboard.mjs +27 -69
- package/src/session-runtime/turn/session-ops.mjs +2 -2
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/usage-stats-api.mjs +6 -1
- package/src/standalone/channel-transport/http-routes.mjs +10 -9
- package/src/standalone/daemon-desktop-runtime.mjs +20 -7
- package/src/standalone/daemon-owner.mjs +117 -25
- package/src/standalone/daemon.mjs +43 -13
- package/src/standalone/session-call-cache.mjs +6 -0
- package/src/standalone/session-protocol.mjs +1 -1
- package/src/standalone/session-service/retention/busy.mjs +7 -2
- package/src/standalone/session-transport/routes.mjs +7 -6
- package/src/tui/app/model-options.mjs +39 -2
- package/src/tui/app/model-picker.mjs +16 -2
- package/src/tui/app/provider-setup-picker.mjs +1 -2
- package/src/tui/dist/index.mjs +385 -104
- package/src/tui/session/notice-text.mjs +0 -1
- package/src/tui/session/session-api/intake/submission.mjs +7 -13
- package/src/tui/session/session-api/integrations.mjs +1 -5
- package/src/tui/session/session-flow/drain.mjs +8 -0
- package/src/tui/session/session-flow/steering.mjs +46 -0
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/turn-stream.mjs +33 -0
- package/src/tui/session/turn.mjs +30 -3
- package/src/ui/statusline.mjs +1 -6
- package/src/ui/streaming-markdown-heal.mjs +24 -7
- package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
- package/src/workflows/default/WORKFLOW.md +9 -13
|
@@ -47,23 +47,68 @@ export function extractCompatReasoningEffort(model) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const ERROR_BODY_LIMIT = 4096;
|
|
51
|
+
// An echoed key prefix this long (or the whole key, if shorter) is redacted.
|
|
52
|
+
const ECHOED_KEY_PREFIX_MIN = 8;
|
|
53
|
+
|
|
54
|
+
// Replaces the request key, and any run of its first ECHOED_KEY_PREFIX_MIN or
|
|
55
|
+
// more characters ("Incorrect API key provided: xai-abcd****"), in `value`.
|
|
56
|
+
function redactRequestKey(value, key) {
|
|
57
|
+
if (!key) return value;
|
|
58
|
+
const min = Math.min(ECHOED_KEY_PREFIX_MIN, key.length);
|
|
59
|
+
let out = '';
|
|
60
|
+
for (let i = 0; i < value.length; ) {
|
|
61
|
+
let n = 0;
|
|
62
|
+
while (n < key.length && value[i + n] === key[n]) n += 1;
|
|
63
|
+
if (n >= min) {
|
|
64
|
+
out += '[redacted]';
|
|
65
|
+
i += n;
|
|
66
|
+
} else {
|
|
67
|
+
out += value[i];
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A refused /models request keeps the provider's own reason (status plus the
|
|
75
|
+
// body's code/type/message) so the UI can say why the list is empty. The
|
|
76
|
+
// message stays `<name> models <status>`; the body rides on `providerError`,
|
|
77
|
+
// with the request key (`apiKey`) redacted from it.
|
|
78
|
+
async function compatModelsError(name, res, apiKey = '') {
|
|
79
|
+
const error = Object.assign(new Error(`${name} models ${res.status}`), { status: res.status });
|
|
80
|
+
try {
|
|
81
|
+
const body = JSON.parse((await res.text()).slice(0, ERROR_BODY_LIMIT));
|
|
82
|
+
const nested = body?.error && typeof body.error === 'object' ? body.error : null;
|
|
83
|
+
const text = (value) => (typeof value === 'string' && value ? redactRequestKey(value, apiKey) : undefined);
|
|
84
|
+
const providerError = {
|
|
85
|
+
message: text(body?.error) ?? text(nested?.message) ?? text(body?.message),
|
|
86
|
+
code: text(nested?.code) ?? text(body?.code),
|
|
87
|
+
type: text(nested?.type) ?? text(body?.type),
|
|
88
|
+
};
|
|
89
|
+
if (providerError.message || providerError.code || providerError.type) error.providerError = providerError;
|
|
90
|
+
} catch {
|
|
91
|
+
// A body that is not JSON adds no detail.
|
|
92
|
+
}
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
|
|
50
96
|
// `keyless` omits the Authorization header entirely (keyless endpoints);
|
|
51
97
|
// `signal` aborts the request together with the list timeout.
|
|
52
98
|
export async function fetchCompatModelItems(provider, { signal = null, keyless = false } = {}) {
|
|
53
99
|
const name = provider.name;
|
|
54
100
|
const timeout = createTimeoutSignal(signal, MODEL_LIST_TIMEOUT_MS, `${name} model list`);
|
|
55
101
|
try {
|
|
102
|
+
const apiKey = keyless ? '' : resolveProviderApiKey(name, provider.config || provider);
|
|
56
103
|
const res = await fetch(`${String(provider.baseURL || '').replace(/\/+$/, '')}/models`, {
|
|
57
104
|
method: 'GET',
|
|
58
105
|
headers: {
|
|
59
|
-
...(keyless
|
|
60
|
-
? {}
|
|
61
|
-
: { Authorization: `Bearer ${resolveProviderApiKey(name, provider.config || provider) || 'no-key'}` }),
|
|
106
|
+
...(keyless ? {} : { Authorization: `Bearer ${apiKey || 'no-key'}` }),
|
|
62
107
|
...(provider.defaultHeaders || {}),
|
|
63
108
|
},
|
|
64
109
|
signal: timeout.signal,
|
|
65
110
|
});
|
|
66
|
-
if (!res.ok) throw
|
|
111
|
+
if (!res.ok) throw await compatModelsError(provider.name, res, apiKey);
|
|
67
112
|
const data = await res.json();
|
|
68
113
|
if (Array.isArray(data?.data)) return data.data;
|
|
69
114
|
if (Array.isArray(data)) return data;
|
|
@@ -9,13 +9,6 @@ export const OPENAI_COMPAT_PRESETS = {
|
|
|
9
9
|
baseURL: 'https://api.x.ai/v1',
|
|
10
10
|
defaultModel: 'grok-4.5',
|
|
11
11
|
},
|
|
12
|
-
// OpenCode Go - low-cost coding-model subscription gateway.
|
|
13
|
-
'opencode-go': {
|
|
14
|
-
baseURL: 'https://opencode.ai/inference/go/v1',
|
|
15
|
-
defaultModel: 'glm-5.2',
|
|
16
|
-
// The gateway expects an application User-Agent (not the SDK default).
|
|
17
|
-
extraHeaders: { 'User-Agent': 'mixdog' },
|
|
18
|
-
},
|
|
19
12
|
openrouter: {
|
|
20
13
|
baseURL: 'https://openrouter.ai/api/v1',
|
|
21
14
|
defaultModel: 'openrouter/auto',
|
|
@@ -14,6 +14,7 @@ import { truncatedCompatStreamError, parseCompletedToolCallArgumentsJson } from
|
|
|
14
14
|
import { incompleteReasonFromEvent, isMaxOutputIncompleteReason } from './lib/responses-terminal-fields.mjs';
|
|
15
15
|
import { responsesUsage } from './openai-compat-response-normalization.mjs';
|
|
16
16
|
import { normalizeGatewayErrorDetail } from './provider-error-kind.mjs';
|
|
17
|
+
import { reportStreamItemStart, responsesItemStart } from './lib/send-callbacks.mjs';
|
|
17
18
|
|
|
18
19
|
// An incomplete response was still billed: carry its reported usage on the
|
|
19
20
|
// error so the provider-boundary accounting records it.
|
|
@@ -194,6 +195,7 @@ function onOutputItemAdded(event, state, ctx) {
|
|
|
194
195
|
state.toolTracker?.mark(event.item);
|
|
195
196
|
state.toolInFlight = true;
|
|
196
197
|
}
|
|
198
|
+
reportStreamItemStart(ctx.onStreamItemStart, responsesItemStart(event.item));
|
|
197
199
|
signal(ctx, state.toolInFlight ? 'tool' : 'semantic');
|
|
198
200
|
}
|
|
199
201
|
|
|
@@ -249,7 +251,11 @@ function onRefusalDone(event, state, ctx) {
|
|
|
249
251
|
|
|
250
252
|
function onOutputItemDone(event, state, ctx) {
|
|
251
253
|
const item = event.item || {};
|
|
252
|
-
if (item.type === 'message')
|
|
254
|
+
if (item.type === 'message') {
|
|
255
|
+
refusalsOf(state).absorbMessage(item);
|
|
256
|
+
// A stream that names the phase only on the finished item.
|
|
257
|
+
reportStreamItemStart(ctx.onStreamItemStart, responsesItemStart(item));
|
|
258
|
+
}
|
|
253
259
|
if (item.type === 'function_call') {
|
|
254
260
|
const itemId = reconcileCompatFunctionCallItem(state, item, ctx.label, ctx.onToolCall);
|
|
255
261
|
// Drop the resolved function item from pendingCalls before
|
|
@@ -243,6 +243,7 @@ async function streamCompatResponses({ provider, params, label, signal, opts, he
|
|
|
243
243
|
onStreamDelta: opts.onStreamDelta,
|
|
244
244
|
onToolCall: opts.onToolCall,
|
|
245
245
|
onTextDelta: opts.onTextDelta,
|
|
246
|
+
onStreamItemStart: opts.onStreamItemStart,
|
|
246
247
|
parseResponsesToolCalls,
|
|
247
248
|
responseOutputText,
|
|
248
249
|
knownToolNames: knownToolNamesFromResponsesTools(params.tools),
|
|
@@ -51,6 +51,7 @@ export async function consumeCompatResponsesStream(
|
|
|
51
51
|
onStreamDelta,
|
|
52
52
|
onToolCall,
|
|
53
53
|
onTextDelta,
|
|
54
|
+
onStreamItemStart,
|
|
54
55
|
parseResponsesToolCalls,
|
|
55
56
|
responseOutputText,
|
|
56
57
|
knownToolNames,
|
|
@@ -89,6 +90,7 @@ export async function consumeCompatResponsesStream(
|
|
|
89
90
|
onStreamDelta: reportProgress,
|
|
90
91
|
onToolCall,
|
|
91
92
|
onTextDelta,
|
|
93
|
+
onStreamItemStart,
|
|
92
94
|
relayLeakText,
|
|
93
95
|
};
|
|
94
96
|
const stampOutcome = createResponsesOutcomeStamper(state, leakedCalls);
|
|
@@ -134,6 +134,7 @@ export async function sendXaiResponses(provider, messages, useModel, tools, opts
|
|
|
134
134
|
onStreamDelta: opts.onStreamDelta,
|
|
135
135
|
onToolCall: opts.onToolCall,
|
|
136
136
|
onTextDelta: opts.onTextDelta,
|
|
137
|
+
onStreamItemStart: opts.onStreamItemStart,
|
|
137
138
|
parseResponsesToolCalls,
|
|
138
139
|
responseOutputText,
|
|
139
140
|
knownToolNames: knownToolNamesFromResponsesTools(params.tools),
|
package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createOutputItemEvents } from './response-events/output-items.mjs';
|
|
7
7
|
import { createTerminalFrameEvents } from './response-events/terminal-frames.mjs';
|
|
8
|
+
import { reportStreamItemStart, responsesItemStart } from '../lib/send-callbacks.mjs';
|
|
8
9
|
|
|
9
|
-
export function createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent }) {
|
|
10
|
+
export function createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent, onStreamItemStart }) {
|
|
10
11
|
const output = createOutputItemEvents({ state, items, meaningful });
|
|
11
12
|
const terminal = createTerminalFrameEvents({ state, items, text, outcome, meaningful });
|
|
12
13
|
|
|
@@ -34,6 +35,7 @@ export function createResponseEvents({ state, items, text, outcome, meaningful,
|
|
|
34
35
|
break;
|
|
35
36
|
case 'response.output_item.added':
|
|
36
37
|
output.onOutputItemAdded(event.item);
|
|
38
|
+
reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
|
|
37
39
|
break;
|
|
38
40
|
case 'response.function_call_arguments.delta':
|
|
39
41
|
case 'response.custom_tool_call_input.delta':
|
|
@@ -54,6 +56,8 @@ export function createResponseEvents({ state, items, text, outcome, meaningful,
|
|
|
54
56
|
break;
|
|
55
57
|
case 'response.output_item.done':
|
|
56
58
|
output.onOutputItemDone(event.item || {});
|
|
59
|
+
// A stream that names the phase only on the finished message.
|
|
60
|
+
if (event.item?.type === 'message') reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
|
|
57
61
|
break;
|
|
58
62
|
case 'response.completed':
|
|
59
63
|
terminal.onCompleted(event);
|
|
@@ -25,13 +25,22 @@ import { createResponseEvents } from './openai-http-sse-response-state/response-
|
|
|
25
25
|
* @param {(text: string) => void} [deps.onTextDelta]
|
|
26
26
|
* @param {(kind: string) => void} deps.meaningful semantic progress (re-arms the idle watchdog)
|
|
27
27
|
* @param {() => void} deps.onServerEvent first-event → semantic-idle handover
|
|
28
|
+
* @param {(detail: object) => void} [deps.onStreamItemStart] a tool call or phased message started
|
|
28
29
|
*/
|
|
29
|
-
export function createHttpSseResponseState({
|
|
30
|
+
export function createHttpSseResponseState({
|
|
31
|
+
body,
|
|
32
|
+
onToolCall,
|
|
33
|
+
onTextDelta,
|
|
34
|
+
onStreamItemStart = null,
|
|
35
|
+
meaningful,
|
|
36
|
+
onServerEvent,
|
|
37
|
+
label,
|
|
38
|
+
}) {
|
|
30
39
|
const state = createResponseState(label);
|
|
31
40
|
const outcome = createResponseOutcome({ state });
|
|
32
41
|
const items = createResponseItems({ state, onToolCall });
|
|
33
42
|
const text = createTextRelay({ state, body, onTextDelta, meaningful, emitToolCall: items.emitToolCall });
|
|
34
|
-
const events = createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent });
|
|
43
|
+
const events = createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent, onStreamItemStart });
|
|
35
44
|
|
|
36
45
|
return {
|
|
37
46
|
handleEvent: events.handleEvent,
|
|
@@ -195,6 +195,7 @@ export async function sendViaHttpSse({
|
|
|
195
195
|
body,
|
|
196
196
|
onToolCall,
|
|
197
197
|
onTextDelta,
|
|
198
|
+
onStreamItemStart: typeof opts?.onStreamItemStart === 'function' ? opts.onStreamItemStart : null,
|
|
198
199
|
meaningful,
|
|
199
200
|
onServerEvent: watchdogs.noteServerEvent,
|
|
200
201
|
label,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { createServer } from 'node:http';
|
|
12
12
|
import { randomBytes } from 'node:crypto';
|
|
13
13
|
import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
|
|
14
|
-
import { OAUTH_PAGE_CONTENT_TYPE, oauthSuccessHtml } from './lib/oauth-page.mjs';
|
|
14
|
+
import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
|
|
15
15
|
import { openLoginUrl } from './lib/open-login-url.mjs';
|
|
16
16
|
|
|
17
17
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
@@ -44,7 +44,8 @@ function _scrubOAuthLoginBody(text) {
|
|
|
44
44
|
export function createOpenAIOAuthLogin(deps) {
|
|
45
45
|
const { clientId, originator, extractAccountId, identityFromTokens, expiryFromAccessToken, saveTokens } = deps;
|
|
46
46
|
|
|
47
|
-
async function exchangeAuthorizationCode({ pkce, code }) {
|
|
47
|
+
async function exchangeAuthorizationCode({ pkce, code, signal = null }) {
|
|
48
|
+
signal?.throwIfAborted();
|
|
48
49
|
const cleanCode = String(code || '').trim();
|
|
49
50
|
if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
|
|
50
51
|
const tokenRes = await fetch(TOKEN_URL, {
|
|
@@ -58,7 +59,9 @@ export function createOpenAIOAuthLogin(deps) {
|
|
|
58
59
|
code_verifier: pkce.verifier,
|
|
59
60
|
}),
|
|
60
61
|
redirect: 'error',
|
|
61
|
-
signal:
|
|
62
|
+
signal: signal
|
|
63
|
+
? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
|
|
64
|
+
: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
62
65
|
});
|
|
63
66
|
if (!tokenRes.ok) {
|
|
64
67
|
const text = await tokenRes.text().catch(() => '');
|
|
@@ -78,6 +81,8 @@ export function createOpenAIOAuthLogin(deps) {
|
|
|
78
81
|
account_id: extractAccountId(json.access_token),
|
|
79
82
|
identity: identityFromTokens(json.id_token, json.access_token),
|
|
80
83
|
};
|
|
84
|
+
// A cancelled login never stores the account it was exchanging.
|
|
85
|
+
signal?.throwIfAborted();
|
|
81
86
|
saveTokens(tokens);
|
|
82
87
|
return tokens;
|
|
83
88
|
}
|
|
@@ -100,11 +105,30 @@ export function createOpenAIOAuthLogin(deps) {
|
|
|
100
105
|
let server = null;
|
|
101
106
|
let timeout = null;
|
|
102
107
|
let finish = null;
|
|
108
|
+
let exchangePromise = null;
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
// Browser and manual callbacks share one exchange: an authorization code is
|
|
111
|
+
// single-use, and a duplicate must not settle the login before the first.
|
|
112
|
+
const acceptCode = (code) => {
|
|
113
|
+
if (exchangePromise) return exchangePromise;
|
|
114
|
+
controller.signal.throwIfAborted();
|
|
115
|
+
// The browser deadline ends when a code arrives; the exchange has its own.
|
|
116
|
+
if (timeout) clearTimeout(timeout);
|
|
117
|
+
timeout = null;
|
|
118
|
+
exchangePromise = exchangeAuthorizationCode({ pkce, code, signal: controller.signal });
|
|
119
|
+
exchangePromise.then(
|
|
120
|
+
(tokens) => finish(tokens),
|
|
121
|
+
(err) => finish(null, err instanceof Error ? err : new Error(String(err)))
|
|
122
|
+
);
|
|
123
|
+
return exchangePromise;
|
|
124
|
+
};
|
|
103
125
|
const waitForCallback = new Promise((resolve, reject) => {
|
|
104
126
|
let settled = false;
|
|
105
127
|
finish = (value, error = null) => {
|
|
106
128
|
if (settled) return;
|
|
107
129
|
settled = true;
|
|
130
|
+
// Work arriving after the login settled (or was cancelled) is rejected.
|
|
131
|
+
controller.abort();
|
|
108
132
|
if (timeout) clearTimeout(timeout);
|
|
109
133
|
try {
|
|
110
134
|
server?.close();
|
|
@@ -122,19 +146,25 @@ export function createOpenAIOAuthLogin(deps) {
|
|
|
122
146
|
return;
|
|
123
147
|
}
|
|
124
148
|
const code = u.searchParams.get('code');
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
res.
|
|
128
|
-
|
|
149
|
+
const stateMatches = u.searchParams.get('state') === state;
|
|
150
|
+
if (!code || !stateMatches) {
|
|
151
|
+
res.writeHead(400, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
|
|
152
|
+
res.end(oauthErrorHtml('The sign-in callback was invalid or did not match this login.'));
|
|
153
|
+
// A request for another login is rejected alone; this login's
|
|
154
|
+
// legitimate callback may still arrive.
|
|
155
|
+
if (stateMatches && !exchangePromise) finish(null);
|
|
129
156
|
return;
|
|
130
157
|
}
|
|
131
|
-
res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
|
|
132
|
-
res.end(oauthSuccessHtml('OpenAI'));
|
|
133
158
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
await acceptCode(code);
|
|
160
|
+
// The login settles (closing the server) before this response is
|
|
161
|
+
// written; do not leave a keep-alive connection bound to it.
|
|
162
|
+
res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
|
|
163
|
+
res.end(oauthSuccessHtml('OpenAI'));
|
|
164
|
+
} catch {
|
|
165
|
+
// The shared exchange reports its error through waitForCallback.
|
|
166
|
+
res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
|
|
167
|
+
res.end(oauthErrorHtml('OpenAI could not complete the token exchange.'));
|
|
138
168
|
}
|
|
139
169
|
});
|
|
140
170
|
timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
|
|
@@ -161,9 +191,8 @@ export function createOpenAIOAuthLogin(deps) {
|
|
|
161
191
|
completeCode: async (input) => {
|
|
162
192
|
const parsed = parseOAuthCodeInput(input, { allowHashState: true });
|
|
163
193
|
if (parsed.state && parsed.state !== state) throw new Error('[openai-oauth] OAuth state mismatch');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return tokens;
|
|
194
|
+
if (!parsed.code) throw new Error('[openai-oauth] authorization code is required');
|
|
195
|
+
return await acceptCode(parsed.code);
|
|
167
196
|
},
|
|
168
197
|
cancel: () => {
|
|
169
198
|
finish?.(null);
|
|
@@ -297,6 +297,7 @@ export async function sendViaWebSocket({
|
|
|
297
297
|
onStreamDelta,
|
|
298
298
|
onToolCall,
|
|
299
299
|
onTextDelta,
|
|
300
|
+
onStreamItemStart: typeof sendOpts?.onStreamItemStart === 'function' ? sendOpts.onStreamItemStart : null,
|
|
300
301
|
state: midState,
|
|
301
302
|
logSuppressedReasoningDeltas,
|
|
302
303
|
traceProvider,
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openai-ws-connect.mjs — how a Responses WebSocket reaches the server.
|
|
3
|
+
*
|
|
4
|
+
* Every pool handshake is a brand-new TCP+TLS connection (the upgrade detaches
|
|
5
|
+
* it from any HTTP keep-alive pool), so a burst of agents starting together
|
|
6
|
+
* pays one DNS resolution and one full TLS handshake per socket. This module
|
|
7
|
+
* removes the repeated work and the long stall:
|
|
8
|
+
*
|
|
9
|
+
* - lookup: one resolution per host, shared by concurrent handshakes and
|
|
10
|
+
* reused for a short TTL (libuv's small thread pool otherwise queues one
|
|
11
|
+
* getaddrinfo per socket behind unrelated file I/O);
|
|
12
|
+
* - TLS resumption: the latest session ticket per host rides the next
|
|
13
|
+
* connect, so later sockets skip the full key exchange;
|
|
14
|
+
* - hedged open: an upgrade that has not opened within the hedge window
|
|
15
|
+
* gets a parallel replacement attempt instead of waiting out the whole
|
|
16
|
+
* acquire timeout. The first leg to open wins; the rest are cancelled.
|
|
17
|
+
* Server decisions (any HTTP status on the upgrade, e.g. 429/401) end
|
|
18
|
+
* the race at once so backoff stays driven by real server signals.
|
|
19
|
+
*/
|
|
20
|
+
import { lookup as dnsLookup } from 'node:dns';
|
|
21
|
+
import net from 'node:net';
|
|
22
|
+
import tls from 'node:tls';
|
|
23
|
+
import { resolveTimeoutMs } from '../stall-policy.mjs';
|
|
24
|
+
|
|
25
|
+
const DNS_TTL_MS = resolveTimeoutMs('MIXDOG_PROVIDER_WS_DNS_TTL_MS', 30_000, { minMs: 1_000, maxMs: 300_000 });
|
|
26
|
+
// Observed production upgrades: p95 ≈ 0.55s, max ≈ 3.4s over 6k handshakes,
|
|
27
|
+
// so a leg still unopened after 3s is almost certainly stuck.
|
|
28
|
+
const WS_HEDGE_MS = resolveTimeoutMs('MIXDOG_PROVIDER_WS_HEDGE_MS', 3_000, { minMs: 500, maxMs: 60_000 });
|
|
29
|
+
|
|
30
|
+
const _dnsCache = new Map();
|
|
31
|
+
|
|
32
|
+
function _lookupKey(hostname, options) {
|
|
33
|
+
return `${hostname}\0${options?.family || 0}\0${options?.hints || 0}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _answer(addresses, options, callback) {
|
|
37
|
+
if (options?.all) {
|
|
38
|
+
callback(null, addresses);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const first = addresses[0];
|
|
42
|
+
callback(null, first.address, first.family);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** dns.lookup-compatible resolver with in-flight coalescing and a short TTL. */
|
|
46
|
+
export function cachedLookup(hostname, options, callback) {
|
|
47
|
+
const opts = typeof options === 'function' ? {} : typeof options === 'number' ? { family: options } : options || {};
|
|
48
|
+
const cb = typeof options === 'function' ? options : callback;
|
|
49
|
+
const key = _lookupKey(hostname, opts);
|
|
50
|
+
const cached = _dnsCache.get(key);
|
|
51
|
+
if (cached?.addresses && cached.expiresAt > Date.now()) {
|
|
52
|
+
_answer(cached.addresses, opts, cb);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (cached?.waiters) {
|
|
56
|
+
cached.waiters.push({ opts, cb });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const record = { waiters: [{ opts, cb }] };
|
|
60
|
+
_dnsCache.set(key, record);
|
|
61
|
+
dnsLookup(hostname, { family: opts.family || 0, hints: opts.hints || 0, all: true }, (err, addresses) => {
|
|
62
|
+
const waiters = record.waiters;
|
|
63
|
+
if (err || !addresses?.length) {
|
|
64
|
+
if (_dnsCache.get(key) === record) _dnsCache.delete(key);
|
|
65
|
+
const failure = err || Object.assign(new Error(`lookup ${hostname} returned no address`), { code: 'ENOTFOUND' });
|
|
66
|
+
for (const waiter of waiters) waiter.cb(failure);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
_dnsCache.set(key, { addresses, expiresAt: Date.now() + DNS_TTL_MS });
|
|
70
|
+
for (const waiter of waiters) _answer(addresses, waiter.opts, waiter.cb);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const _tlsSessions = new Map();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `ws` createConnection: the shared lookup for every socket, plus TLS
|
|
78
|
+
* resumption for secure ones. Mirrors ws's own servername defaulting.
|
|
79
|
+
*/
|
|
80
|
+
export function createWsConnection(options) {
|
|
81
|
+
if (options.protocol === 'ws:' || options.protocol === 'http:') {
|
|
82
|
+
return net.connect({ ...options, path: options.socketPath, lookup: cachedLookup });
|
|
83
|
+
}
|
|
84
|
+
const sessionKey = `${options.host}:${options.port}`;
|
|
85
|
+
const servername =
|
|
86
|
+
options.servername || options.servername === '' ? options.servername : net.isIP(options.host) ? '' : options.host;
|
|
87
|
+
const socket = tls.connect({
|
|
88
|
+
...options,
|
|
89
|
+
path: undefined,
|
|
90
|
+
servername,
|
|
91
|
+
lookup: cachedLookup,
|
|
92
|
+
session: _tlsSessions.get(sessionKey),
|
|
93
|
+
});
|
|
94
|
+
socket.on('session', (session) => _tlsSessions.set(sessionKey, session));
|
|
95
|
+
return socket;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Test-only reset of the resolver and TLS session caches. */
|
|
99
|
+
export function _clearConnectCachesForTest() {
|
|
100
|
+
_dnsCache.clear();
|
|
101
|
+
_tlsSessions.clear();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _abortReason(signal, fallback) {
|
|
105
|
+
return signal?.reason instanceof Error ? signal.reason : new Error(fallback);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Race handshake legs until one opens. `openLeg({ externalSignal })` opens one
|
|
110
|
+
* socket and resolves `{ socket }`. A new leg starts every `hedgeMs` while
|
|
111
|
+
* none has opened and the acquire deadline leaves room; a leg's transport
|
|
112
|
+
* failure only ends the race when nothing else is still connecting.
|
|
113
|
+
*/
|
|
114
|
+
export function openHedged({ openLeg, externalSignal, hedgeMs = WS_HEDGE_MS, deadlineMs, deadlineError }) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const legs = new Set();
|
|
118
|
+
let settled = false;
|
|
119
|
+
let hedgeTimer = null;
|
|
120
|
+
let deadlineTimer = null;
|
|
121
|
+
let lastError = null;
|
|
122
|
+
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
if (hedgeTimer) clearTimeout(hedgeTimer);
|
|
125
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
126
|
+
externalSignal?.removeEventListener?.('abort', onExternalAbort);
|
|
127
|
+
};
|
|
128
|
+
const cancelLegs = (reason) => {
|
|
129
|
+
for (const leg of legs) leg.abort(reason);
|
|
130
|
+
legs.clear();
|
|
131
|
+
};
|
|
132
|
+
const finish = (ok, value) => {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
cleanup();
|
|
136
|
+
(ok ? resolve : reject)(value);
|
|
137
|
+
};
|
|
138
|
+
const fail = (err) => {
|
|
139
|
+
cancelLegs(err);
|
|
140
|
+
finish(false, err);
|
|
141
|
+
};
|
|
142
|
+
function onExternalAbort() {
|
|
143
|
+
fail(_abortReason(externalSignal, 'OpenAI WS handshake aborted'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const scheduleHedge = () => {
|
|
147
|
+
if (settled || !(hedgeMs > 0)) return;
|
|
148
|
+
if (Date.now() - startedAt + hedgeMs >= deadlineMs) return;
|
|
149
|
+
hedgeTimer = setTimeout(() => {
|
|
150
|
+
hedgeTimer = null;
|
|
151
|
+
startLeg();
|
|
152
|
+
}, hedgeMs);
|
|
153
|
+
hedgeTimer.unref?.();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function startLeg() {
|
|
157
|
+
if (settled) return;
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
legs.add(controller);
|
|
160
|
+
Promise.resolve()
|
|
161
|
+
.then(() => openLeg({ externalSignal: controller.signal }))
|
|
162
|
+
.then(
|
|
163
|
+
(opened) => {
|
|
164
|
+
legs.delete(controller);
|
|
165
|
+
if (settled) {
|
|
166
|
+
try {
|
|
167
|
+
opened?.socket?.terminate?.();
|
|
168
|
+
} catch {}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
cancelLegs(new Error('OpenAI WS handshake superseded by a faster attempt'));
|
|
172
|
+
finish(true, opened);
|
|
173
|
+
},
|
|
174
|
+
(err) => {
|
|
175
|
+
legs.delete(controller);
|
|
176
|
+
if (settled || controller.signal.aborted) return;
|
|
177
|
+
lastError = err;
|
|
178
|
+
// An HTTP status is the server's decision for this request
|
|
179
|
+
// (rate limit, auth, upgrade refusal): surface it unchanged.
|
|
180
|
+
if (err?.httpStatus != null || legs.size === 0) fail(err);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
if (hedgeTimer) clearTimeout(hedgeTimer);
|
|
184
|
+
scheduleHedge();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (externalSignal?.aborted) {
|
|
188
|
+
onExternalAbort();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
externalSignal?.addEventListener?.('abort', onExternalAbort, { once: true });
|
|
192
|
+
deadlineTimer = setTimeout(() => fail(lastError?.httpStatus != null ? lastError : deadlineError()), deadlineMs);
|
|
193
|
+
deadlineTimer.unref?.();
|
|
194
|
+
startLeg();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { createHash, randomBytes } from 'node:crypto';
|
|
10
10
|
import { performance } from 'node:perf_hooks';
|
|
11
11
|
import { attachHandshakeFailureHandlers, captureHandshakeUpgrade } from './openai-ws-handshake.mjs';
|
|
12
|
+
import { createWsConnection, openHedged } from './openai-ws-connect.mjs';
|
|
12
13
|
import { codexOriginator, codexUserAgent, codexVersionHeader } from './codex-client-meta.mjs';
|
|
13
14
|
import {
|
|
14
15
|
PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
|
|
@@ -539,7 +540,26 @@ const WS_URL_BY_AUTH = { xai: XAI_WS_URL, 'openai-direct': OPENAI_WS_URL };
|
|
|
539
540
|
/** The auth kind an error label names; anything but xAI/direct is the OAuth (Codex) path. */
|
|
540
541
|
const _wsAuthKind = (auth) => (auth?.type === 'xai' || auth?.type === 'openai-direct' ? auth.type : 'openai-oauth');
|
|
541
542
|
|
|
542
|
-
|
|
543
|
+
function _acquireTimeoutError(auth) {
|
|
544
|
+
return Object.assign(
|
|
545
|
+
new Error(`${_wsErrLabel(_wsAuthKind(auth))} acquire timed out before open (${WS_ACQUIRE_TIMEOUT_MS}ms)`),
|
|
546
|
+
{ code: 'EWSACQUIRETIMEOUT', acquireTimeoutMs: WS_ACQUIRE_TIMEOUT_MS }
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// One acquire = a hedged race of handshake legs under the acquire deadline
|
|
551
|
+
// (openai-ws-connect.mjs): a stuck upgrade is replaced after the hedge window
|
|
552
|
+
// instead of costing the whole timeout plus a retry.
|
|
553
|
+
function _openSocket(args) {
|
|
554
|
+
return openHedged({
|
|
555
|
+
openLeg: ({ externalSignal }) => _openSocketLeg({ ...args, externalSignal }),
|
|
556
|
+
externalSignal: args.externalSignal,
|
|
557
|
+
deadlineMs: WS_ACQUIRE_TIMEOUT_MS,
|
|
558
|
+
deadlineError: () => _acquireTimeoutError(args.auth),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function _openSocketLeg({ auth, sessionToken, externalSignal, cacheKey, codexHeaders }) {
|
|
543
563
|
// `ws` loads on the first socket, not at runtime boot via provider-admin.
|
|
544
564
|
const { default: WebSocket } = await import('ws');
|
|
545
565
|
const headers = _buildHandshakeHeaders({ auth, sessionToken, cacheKey, codexHeaders });
|
|
@@ -586,6 +606,7 @@ async function _openSocket({ auth, sessionToken, externalSignal, cacheKey, codex
|
|
|
586
606
|
headers,
|
|
587
607
|
handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS,
|
|
588
608
|
maxPayload: WS_MAX_INCOMING_FRAME_BYTES,
|
|
609
|
+
createConnection: createWsConnection,
|
|
589
610
|
});
|
|
590
611
|
acquireTimer = setTimeout(() => {
|
|
591
612
|
if (settled) return;
|
|
@@ -597,13 +618,7 @@ async function _openSocket({ auth, sessionToken, externalSignal, cacheKey, codex
|
|
|
597
618
|
try {
|
|
598
619
|
socket.terminate();
|
|
599
620
|
} catch {}
|
|
600
|
-
settle(
|
|
601
|
-
false,
|
|
602
|
-
Object.assign(
|
|
603
|
-
new Error(`${_wsErrLabel(_wsAuthKind(auth))} acquire timed out before open (${WS_ACQUIRE_TIMEOUT_MS}ms)`),
|
|
604
|
-
{ code: 'EWSACQUIRETIMEOUT', acquireTimeoutMs: WS_ACQUIRE_TIMEOUT_MS }
|
|
605
|
-
)
|
|
606
|
-
);
|
|
621
|
+
settle(false, _acquireTimeoutError(auth));
|
|
607
622
|
}, WS_ACQUIRE_TIMEOUT_MS);
|
|
608
623
|
try {
|
|
609
624
|
acquireTimer.unref?.();
|
|
@@ -764,6 +779,52 @@ function _onEntryClosed(poolKey, entry) {
|
|
|
764
779
|
if (!entry.ephemeral) _removeFromPool(poolKey, entry);
|
|
765
780
|
}
|
|
766
781
|
|
|
782
|
+
// Sessionless sends (no poolKey) carry no turn state or response chain of
|
|
783
|
+
// their own, so their connections are shared: one idle bucket per
|
|
784
|
+
// credential + cache key, no owner claim (concurrent sessionless callers
|
|
785
|
+
// each take a different idle socket or open one), and a reused socket starts
|
|
786
|
+
// with an empty chain so no request ever deltas against another caller's
|
|
787
|
+
// response. Without this every sessionless send paid a fresh handshake and
|
|
788
|
+
// closed the socket on release.
|
|
789
|
+
const SHARED_POOL_PREFIX = '\0shared:';
|
|
790
|
+
|
|
791
|
+
function _resetChain(entry) {
|
|
792
|
+
entry.lastResponseId = null;
|
|
793
|
+
entry.lastRequestSansInput = null;
|
|
794
|
+
entry.lastRequestInput = null;
|
|
795
|
+
entry.lastResponseItems = null;
|
|
796
|
+
entry.lastInputLen = 0;
|
|
797
|
+
entry.lastInputPrefixHash = null;
|
|
798
|
+
entry.turnState = null;
|
|
799
|
+
entry.turnStateScope = null;
|
|
800
|
+
entry.turnStateTurnId = null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function _acquireShared({ auth, cacheKey, codexHeaders, externalSignal, acqStart }) {
|
|
804
|
+
const compatibility = _poolCompatibility(auth, cacheKey);
|
|
805
|
+
const sharedKey = `${SHARED_POOL_PREFIX}${compatibility.authIdentity}:${compatibility.cacheKeyIdentity}`;
|
|
806
|
+
const arr = _wsPool.get(sharedKey) || [];
|
|
807
|
+
_pruneStaleEntries(sharedKey, arr, compatibility);
|
|
808
|
+
const idle = await _takeIdleEntry(sharedKey, arr, compatibility, acqStart);
|
|
809
|
+
if (idle) {
|
|
810
|
+
_resetChain(idle);
|
|
811
|
+
return { entry: idle, reused: true, ownerWaitMs: 0 };
|
|
812
|
+
}
|
|
813
|
+
const entry = await _openPoolEntry({
|
|
814
|
+
auth,
|
|
815
|
+
cacheKey,
|
|
816
|
+
codexHeaders,
|
|
817
|
+
externalSignal,
|
|
818
|
+
compatibility,
|
|
819
|
+
ephemeral: false,
|
|
820
|
+
acqStart,
|
|
821
|
+
});
|
|
822
|
+
entry.sharedPoolKey = sharedKey;
|
|
823
|
+
_getPoolArr(sharedKey).push(entry);
|
|
824
|
+
entry.socket.on('close', () => _onEntryClosed(sharedKey, entry));
|
|
825
|
+
return { entry, reused: false, ownerWaitMs: 0 };
|
|
826
|
+
}
|
|
827
|
+
|
|
767
828
|
export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders, forceFresh, externalSignal }) {
|
|
768
829
|
const _acqStart = Date.now();
|
|
769
830
|
_acquireTrace(
|
|
@@ -772,6 +833,9 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
772
833
|
if (externalSignal?.aborted) {
|
|
773
834
|
throw _acquireAbortError(externalSignal);
|
|
774
835
|
}
|
|
836
|
+
if (!poolKey && !forceFresh) {
|
|
837
|
+
return _acquireShared({ auth, cacheKey, codexHeaders, externalSignal, acqStart: _acqStart });
|
|
838
|
+
}
|
|
775
839
|
const ownerClaim = await _claimPoolOwner(poolKey, externalSignal);
|
|
776
840
|
const acquired = (entry, reused) => {
|
|
777
841
|
_bindPoolOwner(poolKey, ownerClaim, entry);
|
|
@@ -825,8 +889,9 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
825
889
|
}
|
|
826
890
|
}
|
|
827
891
|
|
|
828
|
-
export function releaseWebSocket({ entry, poolKey, keep }) {
|
|
892
|
+
export function releaseWebSocket({ entry, poolKey: callerPoolKey, keep }) {
|
|
829
893
|
if (!entry) return;
|
|
894
|
+
const poolKey = callerPoolKey || entry.sharedPoolKey || null;
|
|
830
895
|
entry.busy = false;
|
|
831
896
|
if (!keep || !_isOpen(entry) || !poolKey || entry.ephemeral) {
|
|
832
897
|
try {
|