mixdog 0.8.1 → 0.9.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/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- 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/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Direct API — WebSocket transport via Responses API.
|
|
3
3
|
*
|
|
4
|
-
* Uses the same `sendViaWebSocket` plumbing as openai-oauth
|
|
4
|
+
* Uses the same `sendViaWebSocket` plumbing as openai-oauth, with two
|
|
5
5
|
* differences encoded in the `auth.type === 'openai-direct'` branch inside
|
|
6
6
|
* openai-oauth-ws.mjs:
|
|
7
7
|
* 1. Authorization header: Bearer <OPENAI_API_KEY> (no account_id, no
|
|
@@ -18,7 +18,7 @@ import { enrichModels } from './model-catalog.mjs';
|
|
|
18
18
|
import {
|
|
19
19
|
resolveProviderCacheKey,
|
|
20
20
|
resolveProviderPromptCacheLane,
|
|
21
|
-
} from '../
|
|
21
|
+
} from '../agent-runtime/cache-strategy.mjs';
|
|
22
22
|
|
|
23
23
|
const OPENAI_DIRECT_PRIORITY_MODEL_PATTERNS = Object.freeze([
|
|
24
24
|
/^gpt-5\.5(?:-\d{4}|$)/,
|
|
@@ -67,13 +67,13 @@ export class OpenAIDirectProvider {
|
|
|
67
67
|
promptCacheLane,
|
|
68
68
|
});
|
|
69
69
|
// Public OpenAI API priority support is documented separately from the
|
|
70
|
-
//
|
|
71
|
-
// so gpt-5.4-mini can opt into Priority even when the
|
|
70
|
+
// openai-oauth catalog. Keep this provider's service-tier decision local
|
|
71
|
+
// so gpt-5.4-mini can opt into Priority even when the OAuth catalog does
|
|
72
72
|
// not advertise a Fast tier for its OAuth endpoint.
|
|
73
73
|
applyOpenAIDirectFastTier(body, useModel, opts);
|
|
74
74
|
// Public Responses API supports prompt_cache_retention='24h' at no
|
|
75
75
|
// extra cost (same cached_input_tokens billing as the default 5–10
|
|
76
|
-
// min in-memory cache).
|
|
76
|
+
// min in-memory cache). openai-oauth rejects the parameter, so it's
|
|
77
77
|
// injected only on the direct path. See openai-oauth.mjs:290-294
|
|
78
78
|
// for the rationale.
|
|
79
79
|
body.prompt_cache_retention = '24h';
|
|
@@ -85,7 +85,7 @@ export class OpenAIDirectProvider {
|
|
|
85
85
|
// cacheKey (prompt_cache_key) only groups the server-side prefix-cache
|
|
86
86
|
// shard — safe to share across sessions, unlike the sessionId poolKey
|
|
87
87
|
// above. buildRequestBody derives it from the base namespace plus a
|
|
88
|
-
// model/system/tools hash, mirroring the
|
|
88
|
+
// model/system/tools hash, mirroring the openai-oauth path while keeping
|
|
89
89
|
// public OpenAI's 24h retention below.
|
|
90
90
|
const cacheKey = body.prompt_cache_key || resolveProviderCacheKey(opts, 'openai');
|
|
91
91
|
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { GrokOAuthProvider, hasGrokOAuthCredentials } from './grok-oauth.mjs';
|
|
8
|
-
import { OpenAIDirectProvider } from './openai-ws.mjs';
|
|
1
|
+
import { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
|
|
2
|
+
import {
|
|
3
|
+
hasAnthropicOAuthCredentials,
|
|
4
|
+
hasOpenAIOAuthCredentials,
|
|
5
|
+
hasGrokOAuthCredentials,
|
|
6
|
+
} from './oauth-credential-probes.mjs';
|
|
9
7
|
import { refreshCatalog as refreshMetadataCatalog, warmModelMetadataCatalogs } from './model-catalog.mjs';
|
|
10
|
-
// OpenAI-compat provider names are self-declared by openai-compat.mjs via
|
|
8
|
+
// OpenAI-compat provider names are self-declared by openai-compat-presets.mjs via
|
|
11
9
|
// OPENAI_COMPAT_PRESETS. No parallel list maintained here.
|
|
12
10
|
const providers = new Map();
|
|
11
|
+
const providerCtors = new Map();
|
|
12
|
+
const providerModulePromises = new Map();
|
|
13
13
|
// Parallel map: provider name -> signature of the config it was built from.
|
|
14
14
|
// Lets initProviders() skip reconstructing a provider whose config is byte-for-
|
|
15
15
|
// byte identical to the live one, so lazy-init misses that re-run initProviders
|
|
16
16
|
// don't churn (tear down + rebuild) every live provider instance on every call.
|
|
17
17
|
const signatures = new Map();
|
|
18
18
|
|
|
19
|
+
// Module-level init serialization. agent-tool.mjs's ensureProvider() already
|
|
20
|
+
// serializes inits per provider on a chain promise, but its gateOnPrior() lets
|
|
21
|
+
// a queued init PROCEED once a 120s gate expires even if the prior init has
|
|
22
|
+
// not settled — so a slow-but-still-running init and a newer one can reach
|
|
23
|
+
// initProviders() concurrently. This chain makes the clear()+rebuild section
|
|
24
|
+
// strictly sequential at the registry level, independent of any caller gating,
|
|
25
|
+
// so two different config signatures can never interleave their rebuilds.
|
|
26
|
+
let _initChain = Promise.resolve();
|
|
27
|
+
|
|
19
28
|
// Deterministic structural signature of a provider config. Recursively sorts
|
|
20
29
|
// object keys so signature equality reflects config-value equality regardless
|
|
21
30
|
// of key insertion order. Invariant: same config in -> same string out.
|
|
@@ -37,7 +46,51 @@ function configSignature(cfg) {
|
|
|
37
46
|
return null;
|
|
38
47
|
}
|
|
39
48
|
}
|
|
49
|
+
|
|
50
|
+
async function loadProviderExport(cacheKey, spec, exportName) {
|
|
51
|
+
if (!providerModulePromises.has(cacheKey)) {
|
|
52
|
+
providerModulePromises.set(cacheKey, import(spec));
|
|
53
|
+
}
|
|
54
|
+
const mod = await providerModulePromises.get(cacheKey);
|
|
55
|
+
const value = mod?.[exportName];
|
|
56
|
+
if (typeof value !== 'function') throw new Error(`provider export missing: ${exportName}`);
|
|
57
|
+
providerCtors.set(cacheKey, value);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function loadProviderCtor(name) {
|
|
62
|
+
if (name === 'anthropic') return loadProviderExport('anthropic', './anthropic.mjs', 'AnthropicProvider');
|
|
63
|
+
if (name === 'gemini') return loadProviderExport('gemini', './gemini.mjs', 'GeminiProvider');
|
|
64
|
+
if (name === 'openai-oauth') return loadProviderExport('openai-oauth', './openai-oauth.mjs', 'OpenAIOAuthProvider');
|
|
65
|
+
if (name === 'anthropic-oauth') return loadProviderExport('anthropic-oauth', './anthropic-oauth.mjs', 'AnthropicOAuthProvider');
|
|
66
|
+
if (name === 'grok-oauth') return loadProviderExport('grok-oauth', './grok-oauth.mjs', 'GrokOAuthProvider');
|
|
67
|
+
if (name === 'openai') return loadProviderExport('openai', './openai-ws.mjs', 'OpenAIDirectProvider');
|
|
68
|
+
if (name === 'opencode-go') return loadProviderExport('opencode-go', './opencode-go.mjs', 'OpenCodeGoProvider');
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_PRESETS, name)) {
|
|
70
|
+
return loadProviderExport('openai-compat', './openai-compat.mjs', 'OpenAICompatProvider');
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`unknown enabled provider: ${name}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function instantiateProvider(name, Ctor, cfg) {
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_PRESETS, name) && name !== 'opencode-go') {
|
|
77
|
+
return new Ctor(name, cfg);
|
|
78
|
+
}
|
|
79
|
+
return new Ctor(cfg);
|
|
80
|
+
}
|
|
81
|
+
|
|
40
82
|
export async function initProviders(config) {
|
|
83
|
+
// Serialize ALL inits through a single chain so two different config
|
|
84
|
+
// signatures can never run their clear()+rebuild concurrently, regardless
|
|
85
|
+
// of caller-side gating (agent-tool gateOnPrior may release a queued init
|
|
86
|
+
// before the prior one settled). Errors do not poison the chain.
|
|
87
|
+
const run = () => _initProvidersUnsynchronized(config);
|
|
88
|
+
const next = _initChain.then(run, run);
|
|
89
|
+
_initChain = next.then(() => {}, () => {});
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function _initProvidersUnsynchronized(config) {
|
|
41
94
|
// Invariant: never wipe the live registry based on an empty / all-disabled
|
|
42
95
|
// config. Without this guard, a stale `loadAgentConfig()` (e.g. mid-reload
|
|
43
96
|
// or a transient FS hiccup) would land here as `{}` or `{...,enabled:false}`,
|
|
@@ -51,54 +104,30 @@ export async function initProviders(config) {
|
|
|
51
104
|
}
|
|
52
105
|
const next = new Map();
|
|
53
106
|
const nextSignatures = new Map();
|
|
54
|
-
|
|
55
|
-
if (!cfg.enabled)
|
|
56
|
-
continue;
|
|
107
|
+
const enabledResults = await Promise.all(entries.map(async ([name, cfg]) => {
|
|
108
|
+
if (!cfg.enabled) return null;
|
|
57
109
|
// Idempotent reuse: an enabled provider whose config signature is
|
|
58
110
|
// unchanged from the live registry is carried forward as-is. Only
|
|
59
111
|
// added or changed providers are (re)constructed below.
|
|
60
112
|
const sig = configSignature(cfg);
|
|
61
113
|
if (sig !== null && providers.has(name) && signatures.get(name) === sig) {
|
|
62
|
-
|
|
63
|
-
nextSignatures.set(name, sig);
|
|
64
|
-
continue;
|
|
114
|
+
return { name, inst: providers.get(name), sig };
|
|
65
115
|
}
|
|
66
116
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
else if (name === 'gemini') {
|
|
72
|
-
inst = new GeminiProvider(cfg);
|
|
73
|
-
}
|
|
74
|
-
else if (name === 'openai-oauth') {
|
|
75
|
-
inst = new OpenAIOAuthProvider(cfg);
|
|
76
|
-
}
|
|
77
|
-
else if (name === 'anthropic-oauth') {
|
|
78
|
-
inst = new AnthropicOAuthProvider(cfg);
|
|
79
|
-
}
|
|
80
|
-
else if (name === 'grok-oauth') {
|
|
81
|
-
inst = new GrokOAuthProvider(cfg);
|
|
82
|
-
}
|
|
83
|
-
else if (name === 'openai') {
|
|
84
|
-
inst = new OpenAIDirectProvider(cfg);
|
|
85
|
-
}
|
|
86
|
-
else if (name === 'opencode-go') {
|
|
87
|
-
inst = new OpenCodeGoProvider(cfg);
|
|
88
|
-
}
|
|
89
|
-
else if (Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_PRESETS, name)) {
|
|
90
|
-
inst = new OpenAICompatProvider(name, cfg);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
throw new Error(`unknown enabled provider: ${name}`);
|
|
94
|
-
}
|
|
95
|
-
next.set(name, inst);
|
|
96
|
-
if (sig !== null) nextSignatures.set(name, sig);
|
|
117
|
+
const Ctor = await loadProviderCtor(name);
|
|
118
|
+
const inst = instantiateProvider(name, Ctor, cfg);
|
|
119
|
+
return { name, inst, sig };
|
|
97
120
|
}
|
|
98
121
|
catch (err) {
|
|
99
122
|
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
-
|
|
123
|
+
return { name, error: new Error(`[provider] Failed to init "${name}": ${msg}`) };
|
|
101
124
|
}
|
|
125
|
+
}));
|
|
126
|
+
for (const result of enabledResults) {
|
|
127
|
+
if (!result) continue;
|
|
128
|
+
if (result.error) throw result.error;
|
|
129
|
+
next.set(result.name, result.inst);
|
|
130
|
+
if (result.sig !== null) nextSignatures.set(result.name, result.sig);
|
|
102
131
|
}
|
|
103
132
|
// Second guard: every entry was disabled. Same reasoning — keep the
|
|
104
133
|
// existing registry rather than going dark.
|
|
@@ -136,19 +165,27 @@ export function getProvider(name) {
|
|
|
136
165
|
// failure) — initProviders then skipped the entry entirely so there is
|
|
137
166
|
// nothing for the preservation guard to carry forward. Re-probe the
|
|
138
167
|
// credential each miss: if the credential is now valid, register the
|
|
139
|
-
// instance on the spot
|
|
168
|
+
// instance on the spot when that provider module has already been loaded,
|
|
169
|
+
// so subsequent calls hit the cached entry without making registry import
|
|
170
|
+
// pay every provider runtime at boot.
|
|
140
171
|
if (name === 'anthropic-oauth' && hasAnthropicOAuthCredentials()) {
|
|
141
|
-
const
|
|
172
|
+
const Ctor = providerCtors.get('anthropic-oauth');
|
|
173
|
+
if (!Ctor) return undefined;
|
|
174
|
+
const inst = new Ctor({});
|
|
142
175
|
providers.set(name, inst);
|
|
143
176
|
return inst;
|
|
144
177
|
}
|
|
145
178
|
if (name === 'openai-oauth' && hasOpenAIOAuthCredentials()) {
|
|
146
|
-
const
|
|
179
|
+
const Ctor = providerCtors.get('openai-oauth');
|
|
180
|
+
if (!Ctor) return undefined;
|
|
181
|
+
const inst = new Ctor({});
|
|
147
182
|
providers.set(name, inst);
|
|
148
183
|
return inst;
|
|
149
184
|
}
|
|
150
185
|
if (name === 'grok-oauth' && hasGrokOAuthCredentials()) {
|
|
151
|
-
const
|
|
186
|
+
const Ctor = providerCtors.get('grok-oauth');
|
|
187
|
+
if (!Ctor) return undefined;
|
|
188
|
+
const inst = new Ctor({});
|
|
152
189
|
providers.set(name, inst);
|
|
153
190
|
return inst;
|
|
154
191
|
}
|
|
@@ -173,7 +210,7 @@ export function getAllProviders() {
|
|
|
173
210
|
}
|
|
174
211
|
// Background catalog warm-up. Each provider's listModels() either hits its
|
|
175
212
|
// own cached model list (no-op) or fires a single HTTP refresh. Called from
|
|
176
|
-
// agent.init() after providers are registered so the first
|
|
213
|
+
// agent.init() after providers are registered so the first agent dispatch call
|
|
177
214
|
// (e.g. cycle1 on session start) does not pay the catalog refresh latency
|
|
178
215
|
// inline. Fire-and-forget: failures are logged inside each provider.
|
|
179
216
|
export function warmupCatalogs() {
|
|
@@ -27,10 +27,10 @@ import {
|
|
|
27
27
|
|
|
28
28
|
// HTTP statuses considered transient — safe to retry with backoff.
|
|
29
29
|
// 408 — request timeout
|
|
30
|
-
// 429 — rate limit (caller may still respect Retry-After, but the kind
|
|
31
|
-
// classification here only signals "retryable"; sub-error in the
|
|
32
|
-
// provider can still treat 429 as permanent for quota-exhausted)
|
|
33
30
|
// 500/502/503/504 — server errors (overload / bad gateway / timeout)
|
|
31
|
+
// 429 is deliberately excluded: rate-limit/quota windows are deterministic
|
|
32
|
+
// for the current call and must surface immediately instead of sleeping until
|
|
33
|
+
// an outer watchdog reports a misleading timeout.
|
|
34
34
|
const TRANSIENT_STATUSES = new Set([408, 500, 502, 503, 504])
|
|
35
35
|
|
|
36
36
|
// HTTP statuses that mean "permanent: stop retrying, surface to caller".
|
|
@@ -44,7 +44,7 @@ const PERMANENT_STATUSES = new Set([400, 404, 405, 410, 415, 422])
|
|
|
44
44
|
// payload but no explicit status code — we sniff the text and assign the most
|
|
45
45
|
// likely HTTP equivalent so the retry layer can use the same rules.
|
|
46
46
|
const MESSAGE_PATTERNS = [
|
|
47
|
-
// OpenAI/
|
|
47
|
+
// OpenAI OAuth/API sometimes surfaces generic backend failures only as
|
|
48
48
|
// message text plus a request ID, with no HTTP status on the WS event.
|
|
49
49
|
{ regex: /(?:an error occurred while processing your request|please include the request id)/i, status: 503 },
|
|
50
50
|
// Overload / transient 5xx — server is asking us to back off. The `\b`
|
|
@@ -58,7 +58,7 @@ const MESSAGE_PATTERNS = [
|
|
|
58
58
|
// by classifyError because per-call retry won't change the answer (the
|
|
59
59
|
// window must elapse). Providers that want time-bounded retry should
|
|
60
60
|
// honor Retry-After on the original response, not loop here.
|
|
61
|
-
{ regex: /(?:rate ?limit|quota)/i, status: 429 },
|
|
61
|
+
{ regex: /(?:rate[_ -]?limit|quota|too many requests|resource exhausted|insufficient_quota|quota_exceeded)/i, status: 429 },
|
|
62
62
|
// Auth — never retryable from our side.
|
|
63
63
|
{ regex: /\b(?:unauthorized|unauthorised|authentication|not authenticated|token expired|access token|invalid api key)\b/i, status: 401 },
|
|
64
64
|
{ regex: /\b(?:forbidden|permission denied|policy violation)\b/i, status: 403 },
|
|
@@ -212,6 +212,13 @@ export function isRetryable(err) {
|
|
|
212
212
|
const DEFAULT_BACKOFF_MS = PROVIDER_RETRY_BACKOFF_MS
|
|
213
213
|
const DEFAULT_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS
|
|
214
214
|
|
|
215
|
+
export const MIDSTREAM_BACKOFF_MS = [250, 1000, 2000, 4000]
|
|
216
|
+
|
|
217
|
+
export function midstreamBackoffFor(retryNumber, schedule = MIDSTREAM_BACKOFF_MS) {
|
|
218
|
+
const raw = schedule[Math.min(Math.max(retryNumber, 1), schedule.length) - 1]
|
|
219
|
+
return jitterDelayMs(raw)
|
|
220
|
+
}
|
|
221
|
+
|
|
215
222
|
export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 'symmetric') {
|
|
216
223
|
const base = Number(ms) || 0
|
|
217
224
|
if (base <= 0) return 0
|
|
@@ -224,6 +231,264 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
|
|
|
224
231
|
return Math.max(0, Math.round(base + offset))
|
|
225
232
|
}
|
|
226
233
|
|
|
234
|
+
// ── Shared network-resilience interface ──────────────────────────────────────
|
|
235
|
+
// One home for the logic that used to be copied per provider: mid-stream
|
|
236
|
+
// classifier (WS + SSE), transport fallback predicate, stream-safety stamp
|
|
237
|
+
// latches, abort-aware sleep, handshake classifier, and the retry-budget table.
|
|
238
|
+
// Provider differences are passed as ARGUMENTS (policy objects), never branched
|
|
239
|
+
// on a hardcoded provider name. Behavior is preserved EXACTLY — each path below
|
|
240
|
+
// is the relocated original, selected by policy.mode.
|
|
241
|
+
|
|
242
|
+
// F) Retry-budget profiles as DATA. The numbers live ONLY here now.
|
|
243
|
+
// ws.transientCloseRetries (4) — ws_1006 / ws_1011 connection-loss buckets.
|
|
244
|
+
// ws.defaultRetries (2) — every other WS mid-stream bucket.
|
|
245
|
+
// sse.defaultRetries (3) — anthropic single-shot SSE mid-stream budget.
|
|
246
|
+
export const MIDSTREAM_RETRY_POLICY = {
|
|
247
|
+
ws: { transientCloseRetries: 4, defaultRetries: 2, backoff: [250, 1000, 2000, 4000] },
|
|
248
|
+
sse: { defaultRetries: 3, backoff: [250, 1000, 2000, 4000] },
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// WS buckets that earn the larger transient-close retry budget.
|
|
252
|
+
const WS_TRANSIENT_CLOSE_CLASSIFIERS = new Set(['ws_1006', 'ws_1011'])
|
|
253
|
+
|
|
254
|
+
function _midstreamLimitFor(classifier, policy) {
|
|
255
|
+
if (policy.mode === 'ws') {
|
|
256
|
+
return WS_TRANSIENT_CLOSE_CLASSIFIERS.has(classifier)
|
|
257
|
+
? policy.transientCloseRetries
|
|
258
|
+
: policy.defaultRetries
|
|
259
|
+
}
|
|
260
|
+
return policy.defaultRetries
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// WS gates each classifier against its own budget (mirrors the old
|
|
264
|
+
// _allowMidstreamRetry). SSE applies a single top-of-function budget gate and
|
|
265
|
+
// then returns raw classifier strings, so perClassifierGate:false returns the
|
|
266
|
+
// classifier unconditionally here.
|
|
267
|
+
function _allowMidstream(classifier, attemptIndex, policy) {
|
|
268
|
+
if (policy.perClassifierGate === false) return classifier
|
|
269
|
+
return attemptIndex < _midstreamLimitFor(classifier, policy) ? classifier : null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A) Unified mid-stream classifier. Returns a classifier string or null.
|
|
273
|
+
// `signals` is the provider's mid-stream state object (field names unchanged
|
|
274
|
+
// from each provider's midState). `policy.mode` selects the WS or SSE path so
|
|
275
|
+
// both providers reproduce their exact current branch order and gating.
|
|
276
|
+
export function classifyMidstreamError(err, signals, policy = {}) {
|
|
277
|
+
if (!signals) return null
|
|
278
|
+
const attemptIndex = signals.attemptIndex | 0
|
|
279
|
+
if (policy.mode === 'sse') return _classifyMidstreamSse(err, signals, attemptIndex, policy)
|
|
280
|
+
return _classifyMidstreamWs(err, signals, attemptIndex, policy)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Verbatim relocation of openai-oauth-ws's _classifyMidstreamError. `signals`
|
|
284
|
+
// carries sawResponseCreated / sawCompleted / emittedText / emittedToolCall /
|
|
285
|
+
// wsCloseCode / firstByteTimeout / firstMeaningfulTimeout / wsSendFailed /
|
|
286
|
+
// userAbort / watchdogAbort / responseFailedPayload exactly as before.
|
|
287
|
+
function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
288
|
+
if (state.sawCompleted) return null
|
|
289
|
+
if (state.emittedToolCall) {
|
|
290
|
+
const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
291
|
+
if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null
|
|
292
|
+
}
|
|
293
|
+
if (state.emittedText || err?.liveTextEmitted) return null
|
|
294
|
+
if (state.firstByteTimeout || err?.firstByteTimeout) {
|
|
295
|
+
return _allowMidstream('first_byte_timeout', attemptIndex, policy)
|
|
296
|
+
}
|
|
297
|
+
if (state.firstMeaningfulTimeout || err?.firstMeaningfulTimeout) {
|
|
298
|
+
return _allowMidstream('first_meaningful_timeout', attemptIndex, policy)
|
|
299
|
+
}
|
|
300
|
+
if (err?.wsSendFailed || state.wsSendFailed) {
|
|
301
|
+
return _allowMidstream('ws_send_failed', attemptIndex, policy)
|
|
302
|
+
}
|
|
303
|
+
if (!state.sawResponseCreated) {
|
|
304
|
+
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
305
|
+
if (closeCode !== 1011 && closeCode !== 1012) return null
|
|
306
|
+
}
|
|
307
|
+
if (state.userAbort) return null
|
|
308
|
+
|
|
309
|
+
if (!err) return null
|
|
310
|
+
const status = Number(err?.httpStatus || 0)
|
|
311
|
+
if (status === 401 || status === 403 || status === 429) return null
|
|
312
|
+
if (status >= 500 && status < 600) {
|
|
313
|
+
return _allowMidstream(`http_${status}`, attemptIndex, policy)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const name = err?.name || ''
|
|
317
|
+
if (name === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
|
|
318
|
+
if (name === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
319
|
+
|
|
320
|
+
if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
|
|
321
|
+
if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
322
|
+
|
|
323
|
+
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
324
|
+
if (closeCode === 1006) return _allowMidstream('ws_1006', attemptIndex, policy)
|
|
325
|
+
if (closeCode === 1011) return _allowMidstream('ws_1011', attemptIndex, policy)
|
|
326
|
+
if (closeCode === 1012) return _allowMidstream('ws_1012', attemptIndex, policy)
|
|
327
|
+
if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null
|
|
328
|
+
if (closeCode === 4000) return _allowMidstream('ws_4000', attemptIndex, policy)
|
|
329
|
+
if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstream('ws_1000', attemptIndex, policy)
|
|
330
|
+
|
|
331
|
+
const failed = err?.responseFailed || state.responseFailedPayload
|
|
332
|
+
if (failed) {
|
|
333
|
+
try {
|
|
334
|
+
const blob = JSON.stringify(failed).toLowerCase()
|
|
335
|
+
if (blob.includes('stream_disconnected')) return _allowMidstream('response_failed_disconnected', attemptIndex, policy)
|
|
336
|
+
if (blob.includes('network_error')) return _allowMidstream('response_failed_network', attemptIndex, policy)
|
|
337
|
+
if (blob.includes('auth context expired')) return _allowMidstream('response_failed_auth_expired', attemptIndex, policy)
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Verbatim relocation of anthropic-oauth's _classifyMidstreamError. `signals`
|
|
345
|
+
// carries sawMessageStart / sawCompleted / emittedToolCall / userAbort /
|
|
346
|
+
// watchdogAbort exactly as before.
|
|
347
|
+
function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
348
|
+
if (attemptIndex >= policy.defaultRetries) return null
|
|
349
|
+
if (state.sawCompleted) return null
|
|
350
|
+
if (!state.sawMessageStart) return null
|
|
351
|
+
if (state.userAbort) return null
|
|
352
|
+
if (state.emittedToolCall) return null
|
|
353
|
+
|
|
354
|
+
if (!err) return null
|
|
355
|
+
const status = Number(err?.httpStatus || 0)
|
|
356
|
+
if (status === 401 || status === 403 || status === 429) return null
|
|
357
|
+
|
|
358
|
+
const name = err?.name || ''
|
|
359
|
+
if (name === 'AgentStallAbortError') return 'agent_stall'
|
|
360
|
+
if (name === 'StreamStalledAbortError') return 'stream_stalled'
|
|
361
|
+
if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall'
|
|
362
|
+
if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled'
|
|
363
|
+
|
|
364
|
+
const code = err?.code || err?.cause?.code || ''
|
|
365
|
+
if (code === 'ECONNRESET') return 'reset'
|
|
366
|
+
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
|
|
367
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns'
|
|
368
|
+
|
|
369
|
+
const msg = String(err?.message || '').toLowerCase()
|
|
370
|
+
if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout'
|
|
371
|
+
if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated'
|
|
372
|
+
if (msg.includes('fetch failed')) return 'fetch_failed'
|
|
373
|
+
|
|
374
|
+
return null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// B) Unified transport (WS→HTTP) fallback predicate. Identical deny-order +
|
|
378
|
+
// allow-list to the two former copies; `enabled` replaces the per-provider
|
|
379
|
+
// env-flag check (caller computes the flag and passes it).
|
|
380
|
+
const TRANSPORT_FALLBACK_CLASSIFIERS = new Set([
|
|
381
|
+
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
382
|
+
'first_byte_timeout', 'first_meaningful_timeout',
|
|
383
|
+
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
|
|
384
|
+
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
385
|
+
'ws_send_failed',
|
|
386
|
+
])
|
|
387
|
+
const TRANSPORT_FALLBACK_ERRNO = new Set([
|
|
388
|
+
'EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN',
|
|
389
|
+
'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE',
|
|
390
|
+
])
|
|
391
|
+
|
|
392
|
+
export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
393
|
+
if (!enabled) return false
|
|
394
|
+
if (signal?.aborted) return false
|
|
395
|
+
if (err?.liveTextEmitted === true) return false
|
|
396
|
+
if (err?.emittedToolCall === true || err?.unsafeToRetry === true) return false
|
|
397
|
+
const status = Number(err?.httpStatus || err?.status || 0)
|
|
398
|
+
if (status === 401 || status === 403 || status === 404 || status === 429) return false
|
|
399
|
+
if (status >= 500 && status < 600) return true
|
|
400
|
+
const code = String(err?.code || '')
|
|
401
|
+
if (TRANSPORT_FALLBACK_ERRNO.has(code)) return true
|
|
402
|
+
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '')
|
|
403
|
+
if (TRANSPORT_FALLBACK_CLASSIFIERS.has(classifier)) return true
|
|
404
|
+
if (/^http_5\d\d$/.test(classifier)) return true
|
|
405
|
+
if (err?.firstByteTimeout) return true
|
|
406
|
+
if (err?.firstMeaningfulTimeout) return true
|
|
407
|
+
const msg = String(err?.message || '')
|
|
408
|
+
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// C) Stream-safety stamp latches. Mirrors openai-oauth-ws's _stampLiveText /
|
|
412
|
+
// _stampTool: once text/tool has been marked, every subsequent throw path
|
|
413
|
+
// re-applies the liveTextEmitted/emittedToolCall + unsafeToRetry markers so
|
|
414
|
+
// no upstream gate can reissue the turn and concatenate attempts.
|
|
415
|
+
export function createStreamSafetyStamps() {
|
|
416
|
+
let textLatched = false
|
|
417
|
+
let toolLatched = false
|
|
418
|
+
const stampText = (e) => {
|
|
419
|
+
if (textLatched && e) { try { e.liveTextEmitted = true; e.unsafeToRetry = true } catch {} }
|
|
420
|
+
return e
|
|
421
|
+
}
|
|
422
|
+
const stampTool = (e) => {
|
|
423
|
+
if (toolLatched && e) { try { e.emittedToolCall = true; e.unsafeToRetry = true } catch {} }
|
|
424
|
+
return e
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
markText() { textLatched = true },
|
|
428
|
+
markTool() { toolLatched = true },
|
|
429
|
+
stampText,
|
|
430
|
+
stampTool,
|
|
431
|
+
stampAll: (e) => stampTool(stampText(e)),
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const _defaultAbortSleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
436
|
+
|
|
437
|
+
// D) Abort-aware sleep (single copy). Resolves after `ms`, or rejects with the
|
|
438
|
+
// signal's reason (or `abortMessage`) the moment the signal aborts. `sleepFn`
|
|
439
|
+
// is the injectable no-signal sleep (test seam); `abortMessage` preserves
|
|
440
|
+
// each caller's prior fallback text when the abort reason is not an Error.
|
|
441
|
+
export async function sleepWithAbort(ms, signal, sleepFn = _defaultAbortSleep, abortMessage = 'sleep aborted') {
|
|
442
|
+
if (!ms) return
|
|
443
|
+
if (!signal) {
|
|
444
|
+
await (sleepFn || _defaultAbortSleep)(ms)
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
await new Promise((resolve, reject) => {
|
|
448
|
+
const t = setTimeout(() => {
|
|
449
|
+
try { signal.removeEventListener('abort', onAbort) } catch {}
|
|
450
|
+
resolve()
|
|
451
|
+
}, ms)
|
|
452
|
+
const onAbort = () => {
|
|
453
|
+
clearTimeout(t)
|
|
454
|
+
const reason = signal.reason
|
|
455
|
+
reject(reason instanceof Error ? reason : new Error(abortMessage))
|
|
456
|
+
}
|
|
457
|
+
if (signal.aborted) { onAbort(); return }
|
|
458
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// E) Handshake classifier (moved here from openai-oauth-ws). Default-deny:
|
|
463
|
+
// anything not recognized as transient returns null. 401/403/404/429 are
|
|
464
|
+
// permanent. Returns 'timeout' | 'reset' | 'dns' | 'refused' | 'network' |
|
|
465
|
+
// 'acquire_timeout' | `http_5xx` | null.
|
|
466
|
+
export function classifyHandshakeError(err) {
|
|
467
|
+
if (!err) return null
|
|
468
|
+
const code = err.code || ''
|
|
469
|
+
const msg = String(err.message || '')
|
|
470
|
+
const status = Number(err.httpStatus || 0)
|
|
471
|
+
|
|
472
|
+
if (status === 401 || status === 403 || status === 404 || status === 429) {
|
|
473
|
+
return null
|
|
474
|
+
}
|
|
475
|
+
if (status >= 500 && status < 600) {
|
|
476
|
+
return `http_${status}`
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (code === 'ECONNRESET') return 'reset'
|
|
480
|
+
if (code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA') return 'dns'
|
|
481
|
+
if (code === 'ECONNREFUSED') return 'refused'
|
|
482
|
+
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
|
|
483
|
+
if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout'
|
|
484
|
+
if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network'
|
|
485
|
+
|
|
486
|
+
if (/opening handshake has timed out/i.test(msg)) return 'timeout'
|
|
487
|
+
if (/socket hang up/i.test(msg)) return 'reset'
|
|
488
|
+
|
|
489
|
+
return null
|
|
490
|
+
}
|
|
491
|
+
|
|
227
492
|
/**
|
|
228
493
|
* Run an async function with exponential-backoff retry on transient errors.
|
|
229
494
|
*
|
|
@@ -296,13 +561,17 @@ export async function withRetry(fn, opts = {}) {
|
|
|
296
561
|
}
|
|
297
562
|
lastErr = caught
|
|
298
563
|
populateHttpStatusFromMessage(caught)
|
|
299
|
-
const retryAfterMs = retryAfterMsFromError(caught)
|
|
300
564
|
const status = Number(caught?.httpStatus || caught?.status || caught?.response?.status || 0)
|
|
301
565
|
const kind = classifyError(caught)
|
|
302
|
-
const
|
|
303
|
-
|
|
566
|
+
const unsafeToRetry = caught?.unsafeToRetry === true
|
|
567
|
+
|| caught?.providerQuota === true
|
|
568
|
+
|| caught?.quotaExceeded === true
|
|
569
|
+
if (unsafeToRetry) throw caught
|
|
570
|
+
if (status === 429) throw caught
|
|
571
|
+
if (kind !== 'transient') throw caught
|
|
304
572
|
// Last attempt failed transiently — propagate to caller.
|
|
305
573
|
if (attempt === maxAttempts - 1) throw caught
|
|
574
|
+
const retryAfterMs = retryAfterMsFromError(caught)
|
|
306
575
|
if (retryAfterMs != null) {
|
|
307
576
|
nextDelayMs = Math.max(0, Math.min(retryAfterMs, maxRetryAfterMs))
|
|
308
577
|
nextDelayReason = 'retry-after'
|
|
@@ -316,7 +585,7 @@ export async function withRetry(fn, opts = {}) {
|
|
|
316
585
|
}
|
|
317
586
|
|
|
318
587
|
function _sleepWithAbort(ms, signal) {
|
|
319
|
-
return new Promise((resolve) => {
|
|
588
|
+
return new Promise((resolve, reject) => {
|
|
320
589
|
let onAbort = null
|
|
321
590
|
const t = setTimeout(() => {
|
|
322
591
|
if (signal && onAbort) {
|
|
@@ -325,8 +594,17 @@ function _sleepWithAbort(ms, signal) {
|
|
|
325
594
|
resolve()
|
|
326
595
|
}, ms)
|
|
327
596
|
if (!signal) return
|
|
328
|
-
if (signal.aborted) {
|
|
329
|
-
|
|
597
|
+
if (signal.aborted) {
|
|
598
|
+
clearTimeout(t)
|
|
599
|
+
const reason = signal.reason
|
|
600
|
+
reject(reason instanceof Error ? reason : new Error('sleep aborted'))
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
onAbort = () => {
|
|
604
|
+
clearTimeout(t)
|
|
605
|
+
const reason = signal.reason
|
|
606
|
+
reject(reason instanceof Error ? reason : new Error('sleep aborted'))
|
|
607
|
+
}
|
|
330
608
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
331
609
|
})
|
|
332
610
|
}
|
|
@@ -1,11 +1,44 @@
|
|
|
1
|
-
let routeMeta;
|
|
1
|
+
let routeMeta = null;
|
|
2
|
+
let routeMetaPromise = null;
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
routeMeta
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
async function loadRouteMetaModule() {
|
|
5
|
+
if (routeMeta) return routeMeta;
|
|
6
|
+
if (!routeMetaPromise) {
|
|
7
|
+
routeMetaPromise = import('../../../../vendor/statusline/src/gateway/route-meta.mjs').then((mod) => {
|
|
8
|
+
routeMeta = mod;
|
|
9
|
+
return mod;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return await routeMetaPromise;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
routeMetaPromise = null;
|
|
16
|
+
routeMeta = null;
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
7
19
|
}
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
21
|
+
/** Await before calling the sync exports when route-meta may not be ready yet. */
|
|
22
|
+
export async function ensureStatuslineRouteMetaLoaded() {
|
|
23
|
+
return loadRouteMetaModule();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireRouteMeta() {
|
|
27
|
+
if (!routeMeta) {
|
|
28
|
+
void loadRouteMetaModule().catch(() => {});
|
|
29
|
+
throw new Error('statusline route-meta not loaded');
|
|
30
|
+
}
|
|
31
|
+
return routeMeta;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildGatewayLimits(routeInfo, providerOut = null, usageSnapshot = null) {
|
|
35
|
+
return requireRouteMeta().buildGatewayLimits(routeInfo, providerOut, usageSnapshot);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function recordGatewayUsageEvent(summary) {
|
|
39
|
+
return requireRouteMeta().recordGatewayUsageEvent(summary);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function summarizeGatewayUsage(routeInfo, providerOut, compact = null, durationMs = null) {
|
|
43
|
+
return requireRouteMeta().summarizeGatewayUsage(routeInfo, providerOut, compact, durationMs);
|
|
44
|
+
}
|