mixdog 1.2.1 → 1.2.2
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/gate-local.mjs +19 -3
- package/scripts/model-catalog-audit.mjs +1 -15
- package/src/agents/reviewer/AGENT.md +3 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +1 -0
- package/src/repl/slash-commands.mjs +16 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
- package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
- package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
- package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
- package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
- package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
- package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
- package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/memory/lib/pg/process.mjs +34 -24
- package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
- package/src/runtime/office/README.md +1 -1
- package/src/runtime/office/shared/values.mjs +0 -5
- package/src/runtime/shared/background-tasks.mjs +92 -14
- package/src/runtime/shared/err-text.mjs +25 -0
- package/src/runtime/shared/error-presentation.mjs +159 -19
- package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
- package/src/runtime/shared/llm/model-catalog.mjs +53 -40
- package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
- package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
- package/src/runtime/shared/model-display.mjs +13 -0
- package/src/runtime/shared/quota-resume-store.mjs +11 -0
- package/src/runtime/shared/session-baseline-store.mjs +960 -0
- package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
- package/src/runtime/shared/task-notification-envelope.mjs +2 -2
- package/src/runtime/shared/turn-changes.mjs +447 -219
- package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
- package/src/runtime/shared/turn-snapshot.mjs +170 -48
- package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
- package/src/runtime/shared/worktrees/contract.mjs +0 -24
- package/src/runtime/shared/worktrees/test-support.mjs +19 -4
- package/src/session-runtime/boot/apis.mjs +6 -0
- package/src/session-runtime/boot/infrastructure.mjs +6 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/connect.mjs +29 -9
- package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
- package/src/session-runtime/mcp-glue.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +174 -0
- package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
- package/src/session-runtime/model-route/set-route.mjs +83 -15
- package/src/session-runtime/model-route/web-search-route.mjs +1 -1
- package/src/session-runtime/native-web-search/routes.mjs +22 -3
- package/src/session-runtime/provider-models/model-meta.mjs +12 -3
- package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
- package/src/session-runtime/quick-web-search-models.mjs +13 -38
- package/src/session-runtime/resource-extension-sync.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +98 -2
- package/src/session-runtime/runtime-review-api.mjs +13 -0
- package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
- package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
- package/src/session-runtime/settings-compaction-api.mjs +1 -0
- package/src/standalone/daemon-drain-policy.mjs +8 -0
- package/src/standalone/daemon-telemetry.mjs +55 -0
- package/src/standalone/daemon.mjs +3 -7
- package/src/standalone/session-protocol.mjs +7 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
- package/src/standalone/session-service/agent-tree.mjs +12 -0
- package/src/standalone/session-service/quota-resume.mjs +8 -0
- package/src/standalone/session-service/retention/busy.mjs +15 -1
- package/src/standalone/session-service-api.mjs +5 -0
- package/src/standalone/session-service.mjs +1 -0
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/create-app-pickers.mjs +4 -0
- package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
- package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
- package/src/tui/app/extension-pickers.mjs +2 -1
- package/src/tui/app/model-options.mjs +11 -16
- package/src/tui/app/slash-commands.mjs +7 -2
- package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
- package/src/tui/components/StatusLine.jsx +2 -0
- package/src/tui/components/status-line/local-line.mjs +5 -2
- package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
- package/src/tui/dist/index.mjs +816 -43
- package/src/tui/session/quota-resume.mjs +46 -14
- package/src/tui/session/session-api/commands.mjs +33 -24
- package/src/tui/session/session-api/extensions.mjs +22 -0
- package/src/tui/session/session-api/integrations.mjs +7 -0
- package/src/tui/session/session-api/lifecycle.mjs +5 -1
- package/src/tui/session/session-api/routes.mjs +71 -7
- package/src/tui/session/session-api/settings.mjs +5 -14
- package/src/tui/session/session-flow/drain.mjs +7 -1
- package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
- package/src/tui/session/turn.mjs +38 -15
- package/src/tui/statusline-ansi-bridge.mjs +1 -1
- package/src/ui/statusline-agents.mjs +15 -4
- package/src/ui/statusline.mjs +7 -3
- package/src/ui/streaming-markdown-heal.mjs +34 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
- package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
- package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
|
@@ -7,17 +7,16 @@ import { noteErrorUsage } from './lib/note-error-usage.mjs';
|
|
|
7
7
|
import { retryReasonText, retryStatusLine } from '../../../shared/err-text.mjs';
|
|
8
8
|
import {
|
|
9
9
|
TERMINAL_EDGE_STATUSES,
|
|
10
|
-
headerValue,
|
|
11
10
|
boundedCauseChain,
|
|
12
11
|
classifyError,
|
|
13
|
-
isPermanentQuotaError,
|
|
14
12
|
isStaleKeepAliveError,
|
|
15
13
|
isToolInputCut,
|
|
16
14
|
retryAfterMsFromError,
|
|
17
15
|
} from './retry-classification.mjs';
|
|
16
|
+
import { DETERMINISTIC_KINDS, QUOTA_KINDS, errorHeaderValue, providerErrorKind } from './provider-error-kind.mjs';
|
|
17
|
+
import { adviceGatedRetry } from './retry-midstream.mjs';
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
AnthropicFallbackTriggeredError,
|
|
19
|
+
ModelFallbackTriggeredError,
|
|
21
20
|
DEFAULT_BACKOFF_MS,
|
|
22
21
|
DEFAULT_MAX_ATTEMPTS,
|
|
23
22
|
jitterDelayMs,
|
|
@@ -64,11 +63,16 @@ export function createStreamSafetyStamps() {
|
|
|
64
63
|
|
|
65
64
|
const providerRecoveryExhaustedErrors = new WeakSet();
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
// `terminal: true` records a provider VERDICT that recovery is over (a policy
|
|
67
|
+
// veto such as an unadvised advice-gated overload, or the repeated-overload
|
|
68
|
+
// stop), not merely a spent ladder: outer layers end recovery on it instead
|
|
69
|
+
// of opening another budget.
|
|
70
|
+
export function markProviderRecoveryExhausted(error, { owner = 'provider', attempts = null, terminal = false } = {}) {
|
|
68
71
|
if (!error || (typeof error !== 'object' && typeof error !== 'function')) return error;
|
|
69
72
|
providerRecoveryExhaustedErrors.add(error);
|
|
70
73
|
try {
|
|
71
74
|
error.providerRecoveryExhausted = true;
|
|
75
|
+
if (terminal) error.providerRecoveryTerminal = true;
|
|
72
76
|
if (owner) error.providerRecoveryOwner = String(owner);
|
|
73
77
|
const count = Number(attempts);
|
|
74
78
|
if (Number.isFinite(count) && count > 0) error.providerRecoveryAttempts = Math.floor(count);
|
|
@@ -85,18 +89,25 @@ export function isProviderRecoveryExhausted(error) {
|
|
|
85
89
|
);
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/** True when a provider layer ruled that no further recovery may run (see markProviderRecoveryExhausted). */
|
|
93
|
+
export function isProviderRecoveryTerminal(error) {
|
|
94
|
+
if (!error || (typeof error !== 'object' && typeof error !== 'function')) return false;
|
|
95
|
+
return boundedCauseChain(error).some((candidate) => candidate?.providerRecoveryTerminal === true);
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
function throwIfRetryAborted(signal) {
|
|
89
99
|
if (!signal?.aborted) return;
|
|
90
100
|
const reason = signal.reason;
|
|
91
101
|
throw reason instanceof Error ? reason : new Error('withRetry: aborted');
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
104
|
+
// Before a retry: a stale keep-alive socket rebuilds the HTTP client to escape
|
|
105
|
+
// a poisoned keep-alive pool. A caller whose upstream poisons its HTTP/2 pool
|
|
106
|
+
// on server faults (`recycleDispatcherOn5xx`) also rebuilds it on a retried
|
|
107
|
+
// 5xx (edge 522/523/524 included). Never runs for a failure that is not retried.
|
|
108
|
+
function recycleDispatcherAfter(caught, status, opts) {
|
|
98
109
|
const poisoned =
|
|
99
|
-
isStaleKeepAliveError(caught) || (status >= 500 && status < 600
|
|
110
|
+
isStaleKeepAliveError(caught) || (opts.recycleDispatcherOn5xx === true && status >= 500 && status < 600);
|
|
100
111
|
if (!poisoned) return;
|
|
101
112
|
try {
|
|
102
113
|
recycleLlmDispatcher();
|
|
@@ -105,23 +116,71 @@ function recycleDispatcherAfter(caught, status) {
|
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
|
|
119
|
+
// Opt-in: without a fallback model, the request loop stops after
|
|
120
|
+
// `overloadFallbackAfter` overloads instead of spending the whole
|
|
121
|
+
// attempt budget. A provider verdict: no outer layer re-opens recovery.
|
|
122
|
+
function repeated529Error(caught, opts, maxAttempts) {
|
|
123
|
+
const error = new Error('Repeated 529 Overloaded errors', { cause: caught });
|
|
124
|
+
error.name = 'Repeated529Error';
|
|
125
|
+
error.repeated529 = true;
|
|
126
|
+
error.status = Number(caught?.status || caught?.httpStatus || 529) || 529;
|
|
127
|
+
error.httpStatus = error.status;
|
|
128
|
+
error.code = 'overloaded_error';
|
|
129
|
+
if (caught?.headers) error.headers = caught.headers;
|
|
130
|
+
return markProviderRecoveryExhausted(error, {
|
|
131
|
+
owner: opts.recoveryOwner || 'withRetry',
|
|
132
|
+
attempts: maxAttempts,
|
|
133
|
+
terminal: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Deterministic refusals of this request (auth, permission, policy, invalid
|
|
138
|
+
// request, context/size) end the loop before any status, header or overload
|
|
139
|
+
// rule may re-issue it. Quota kinds are judged separately below.
|
|
140
|
+
function deterministicRefusal(errorKind, kind, status, opts) {
|
|
141
|
+
if (!DETERMINISTIC_KINDS.has(errorKind) || QUOTA_KINDS.has(errorKind)) return false;
|
|
142
|
+
// Opt-in: a status-only 499 is retried (classifyError's retry499 verdict;
|
|
143
|
+
// a typed code on the 499 keeps it final).
|
|
144
|
+
return !(status === 499 && opts.retry499 === true && kind === 'transient');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The overload counter (`overloadFallbackAfter`) counts every overload of the
|
|
148
|
+
// send; intervening non-overload failures do not reset it.
|
|
149
|
+
function isOverloadFailure(status, errorKind) {
|
|
150
|
+
return status === 529 || errorKind === 'overloaded';
|
|
151
|
+
}
|
|
152
|
+
|
|
108
153
|
// Decides whether `caught` may be re-issued: returns the error to throw (the
|
|
109
154
|
// caught one, the exhausted-marked one on the last attempt, or the model
|
|
110
|
-
// fallback trigger) or null when the next attempt may run
|
|
111
|
-
|
|
155
|
+
// fallback trigger) or null when the next attempt may run — and only then
|
|
156
|
+
// rebuilds a poisoned HTTP pool for it.
|
|
157
|
+
function retryVeto(caught, context) {
|
|
158
|
+
const verdict = retryVerdict(caught, context);
|
|
159
|
+
if (verdict === null) {
|
|
160
|
+
const status = Number(caught?.httpStatus || caught?.status || caught?.response?.status || 0);
|
|
161
|
+
recycleDispatcherAfter(caught, status, context.opts);
|
|
162
|
+
}
|
|
163
|
+
return verdict;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function retryVerdict(caught, { opts, attempt, maxAttempts, state }) {
|
|
112
167
|
const lastAttempt = attempt === maxAttempts - 1;
|
|
168
|
+
// `finalOnExhaust`: the caller's ladder is the whole recovery, so a spent
|
|
169
|
+
// ladder is a provider-terminal verdict no outer layer re-opens.
|
|
113
170
|
const exhaustedError = () =>
|
|
114
|
-
maxAttempts > 1
|
|
171
|
+
maxAttempts > 1 || opts.finalOnExhaust === true
|
|
115
172
|
? markProviderRecoveryExhausted(caught, {
|
|
116
173
|
owner: opts.recoveryOwner || 'withRetry',
|
|
117
174
|
attempts: maxAttempts,
|
|
175
|
+
terminal: opts.finalOnExhaust === true,
|
|
118
176
|
})
|
|
119
177
|
: caught;
|
|
120
178
|
// A nested provider layer already spent its complete recovery budget.
|
|
121
179
|
// Never multiply that budget by replaying it from an outer withRetry.
|
|
122
180
|
if (isProviderRecoveryExhausted(caught)) return caught;
|
|
123
181
|
const status = Number(caught?.httpStatus || caught?.status || caught?.response?.status || 0);
|
|
124
|
-
const kind = classifyError(caught);
|
|
182
|
+
const kind = classifyError(caught, { retry499: opts.retry499 === true });
|
|
183
|
+
const errorKind = providerErrorKind(caught).kind;
|
|
125
184
|
// Hard replay boundary: a retry RE-ISSUES the request, so it is denied
|
|
126
185
|
// once visible output was relayed or a tool call was dispatched. Whether
|
|
127
186
|
// an eligible failure is actually retried remains the typed question
|
|
@@ -131,40 +190,67 @@ function retryVeto(caught, { opts, attempt, maxAttempts, state }) {
|
|
|
131
190
|
// split-call notice; re-issuing the identical request here would tend to be
|
|
132
191
|
// cut at the same place again.
|
|
133
192
|
if (isToolInputCut(caught)) return caught;
|
|
134
|
-
|
|
193
|
+
const overload = isOverloadFailure(status, errorKind);
|
|
194
|
+
// Deterministic refusals first: no status, header (x-should-retry:true
|
|
195
|
+
// included) or overload rule below may re-issue them.
|
|
196
|
+
if (deterministicRefusal(errorKind, kind, status, opts)) return caught;
|
|
197
|
+
// An origin-TLS edge failure (525/526) is final whatever header, advice or
|
|
198
|
+
// overload code accompanies it.
|
|
199
|
+
if (TERMINAL_EDGE_STATUSES.has(status)) return caught;
|
|
200
|
+
// An explicit quota / plan / billing refusal cannot recover by replaying
|
|
201
|
+
// the same request, whatever Retry-After the server attached. A 429 whose
|
|
202
|
+
// only quota evidence is header-derived (providerQuota / quotaExceeded) is
|
|
203
|
+
// still retried for non-subscriber callers (Enterprise/PAYG
|
|
204
|
+
// 429s); a typed quota/billing code never is.
|
|
205
|
+
if (QUOTA_KINDS.has(errorKind)) {
|
|
206
|
+
const typedQuota = QUOTA_KINDS.has(providerErrorKind(caught, { quotaFlags: false }).kind);
|
|
207
|
+
if (typedQuota || opts.retry429 === false || status !== 429) return caught;
|
|
208
|
+
}
|
|
135
209
|
// Background calls (titles and the like) never retry an overload: during a
|
|
136
210
|
// capacity cascade every retry multiplies gateway load, and nobody is
|
|
137
211
|
// waiting on the result.
|
|
138
212
|
if (status === 529 && opts.retry529 === false) return caught;
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// classification. Keep subscription OAuth 429 fail-fast ownership:
|
|
148
|
-
// retry429:false is the Max/Pro gate and must not wait for that window.
|
|
149
|
-
if (opts.provider === 'anthropic' && shouldRetryHeader === 'true' && !(status === 429 && opts.retry429 === false)) {
|
|
150
|
-
return lastAttempt ? exhaustedError() : null;
|
|
213
|
+
// A caller may opt into advice-gated overload / generic-429 retries. The
|
|
214
|
+
// denial is the provider's verdict: outer recovery layers honor it.
|
|
215
|
+
if (adviceGatedRetry(caught, opts) === false) {
|
|
216
|
+
return markProviderRecoveryExhausted(caught, {
|
|
217
|
+
owner: opts.recoveryOwner || 'withRetry',
|
|
218
|
+
attempts: attempt + 1,
|
|
219
|
+
terminal: true,
|
|
220
|
+
});
|
|
151
221
|
}
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
222
|
+
// Overload first: counted and retried ahead of the x-should-retry veto.
|
|
223
|
+
// The optional model fallback fires after `overloadFallbackAfter`
|
|
224
|
+
// overloads in the send (opt-in: providers pass fallbackModel only when
|
|
225
|
+
// the caller set one); without one, `stopAfterRepeated529` ends the loop
|
|
226
|
+
// there instead. The hard progress veto above runs first so neither can
|
|
227
|
+
// replay partial thinking/tool output. `overloadState` shares the count
|
|
228
|
+
// across a send.
|
|
229
|
+
if (overload) {
|
|
230
|
+
// Subscriber 429 fail-fast ownership also covers an overload-coded 429.
|
|
231
|
+
if (status === 429 && opts.retry429 === false) return caught;
|
|
157
232
|
state.consecutive529Errors += 1;
|
|
158
|
-
|
|
159
|
-
|
|
233
|
+
const limit = Number(opts.overloadFallbackAfter);
|
|
234
|
+
if (Number.isFinite(limit) && limit > 0 && state.consecutive529Errors >= limit) {
|
|
235
|
+
if (opts.fallbackModel && opts.fallbackModel !== opts.model) {
|
|
236
|
+
return new ModelFallbackTriggeredError(opts.model, opts.fallbackModel, caught);
|
|
237
|
+
}
|
|
238
|
+
if (opts.stopAfterRepeated529 === true) return repeated529Error(caught, opts, maxAttempts);
|
|
160
239
|
}
|
|
240
|
+
return lastAttempt ? exhaustedError() : null;
|
|
241
|
+
}
|
|
242
|
+
// x-should-retry:false is an explicit server veto on retrying and is
|
|
243
|
+
// honored as-is, ahead of status defaults including the 429 path.
|
|
244
|
+
const shouldRetryHeader = String(errorHeaderValue(caught, 'x-should-retry') || '').toLowerCase();
|
|
245
|
+
if (shouldRetryHeader === 'false') return caught;
|
|
246
|
+
// A transport that opts in (honorShouldRetryTrue) lets the non-standard
|
|
247
|
+
// positive override outrank ordinary status classification — except for
|
|
248
|
+
// subscribers (retry429:false), whose fail-fast ownership holds for every status.
|
|
249
|
+
if (opts.honorShouldRetryTrue === true && shouldRetryHeader === 'true' && opts.retry429 !== false) {
|
|
250
|
+
return lastAttempt ? exhaustedError() : null;
|
|
161
251
|
}
|
|
162
252
|
if (status === 429) {
|
|
163
253
|
if (opts.retry429 === false) return caught;
|
|
164
|
-
// A deterministic quota refusal cannot recover by replaying the same
|
|
165
|
-
// request. An explicit server retry window outranks message-text quota
|
|
166
|
-
// heuristics: RESOURCE_EXHAUSTED + Retry-After/RetryInfo is transient.
|
|
167
|
-
if (retryAfterMsFromError(caught) == null && isPermanentQuotaError(caught)) return caught;
|
|
168
254
|
// Retry only this request. Admission concurrency is fixed and is never
|
|
169
255
|
// reduced by rate limits. Output/tool stamps above remain a hard replay
|
|
170
256
|
// boundary.
|
|
@@ -184,8 +270,12 @@ function retryOptions(opts) {
|
|
|
184
270
|
perAttemptTimeoutMs: Number(opts.perAttemptTimeoutMs || 0),
|
|
185
271
|
perAttemptLabel: opts.perAttemptLabel || 'provider request',
|
|
186
272
|
retryJitterRatio: Number(opts.retryJitterRatio ?? PROVIDER_RETRY_JITTER_RATIO),
|
|
187
|
-
retryJitterMode:
|
|
273
|
+
retryJitterMode: ['positive', 'negative'].includes(opts.retryJitterMode) ? opts.retryJitterMode : 'symmetric',
|
|
188
274
|
sleepFn: typeof opts.sleepFn === 'function' ? opts.sleepFn : undefined,
|
|
275
|
+
// Opt-in: positive jitter on server advice, and advice as a
|
|
276
|
+
// floor under — never a replacement for — the current backoff step.
|
|
277
|
+
retryAfterJitterRatio: Math.max(0, Number(opts.retryAfterJitterRatio) || 0),
|
|
278
|
+
adviceAtLeastBackoff: opts.adviceAtLeastBackoff === true,
|
|
189
279
|
};
|
|
190
280
|
}
|
|
191
281
|
|
|
@@ -193,11 +283,17 @@ function retryOptions(opts) {
|
|
|
193
283
|
// the jittered backoff step. Retry-After is a server-mandated minimum. Do
|
|
194
284
|
// not cap, shorten, or jitter it; cancellation remains active throughout
|
|
195
285
|
// the full wait.
|
|
196
|
-
function retryWaitMs(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
286
|
+
function retryWaitMs(
|
|
287
|
+
attempt,
|
|
288
|
+
{ nextDelayMs, nextDelayReason },
|
|
289
|
+
{ backoffMs, retryJitterRatio, retryJitterMode, retryAfterJitterRatio = 0, adviceAtLeastBackoff = false }
|
|
290
|
+
) {
|
|
291
|
+
const step = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? 0;
|
|
292
|
+
if (nextDelayReason !== 'retry-after') return jitterDelayMs(nextDelayMs ?? step, retryJitterRatio, retryJitterMode);
|
|
293
|
+
let wait = Math.max(0, nextDelayMs ?? 0);
|
|
294
|
+
if (adviceAtLeastBackoff) wait = Math.max(wait, jitterDelayMs(step, retryJitterRatio, retryJitterMode));
|
|
295
|
+
// Positive-only jitter never shortens the advice.
|
|
296
|
+
return retryAfterJitterRatio > 0 ? jitterDelayMs(wait, retryAfterJitterRatio, 'positive') : wait;
|
|
201
297
|
}
|
|
202
298
|
|
|
203
299
|
/**
|
|
@@ -260,7 +356,14 @@ export async function withRetry(fn, opts = {}) {
|
|
|
260
356
|
let lastErr = null;
|
|
261
357
|
let nextDelayMs = null;
|
|
262
358
|
let nextDelayReason = null;
|
|
263
|
-
|
|
359
|
+
// `opts.overloadState` ({ consecutive529Errors }) lets one logical send share
|
|
360
|
+
// its overload count across every withRetry it runs (streaming, recovery
|
|
361
|
+
// cycles, non-streaming fallback), so the configured fallback still fires.
|
|
362
|
+
const shared = opts.overloadState && typeof opts.overloadState === 'object' ? opts.overloadState : null;
|
|
363
|
+
const state = shared || {};
|
|
364
|
+
if (!Number.isFinite(state.consecutive529Errors)) {
|
|
365
|
+
state.consecutive529Errors = Math.max(0, Number(opts.initialConsecutive529Errors) || 0);
|
|
366
|
+
}
|
|
264
367
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
265
368
|
throwIfRetryAborted(signal);
|
|
266
369
|
if (attempt > 0) {
|
|
@@ -11,6 +11,46 @@ import {
|
|
|
11
11
|
isTransientErrorCode,
|
|
12
12
|
typedStatusFrom,
|
|
13
13
|
} from './retry-classification.mjs';
|
|
14
|
+
import { DETERMINISTIC_KINDS, providerErrorKind } from './provider-error-kind.mjs';
|
|
15
|
+
|
|
16
|
+
// A 429 that is an overload or an ordinary rate limit is request-local: it is
|
|
17
|
+
// re-issued under the bounded mid-stream budget (the server's advice, when it
|
|
18
|
+
// sent one, sets the wait). Quota / plan / billing refusals never are.
|
|
19
|
+
// The advice-honoring mid-stream wait lives with the backoff schedule.
|
|
20
|
+
export { midstreamRetryDelayMs } from './retry-backoff.mjs';
|
|
21
|
+
|
|
22
|
+
// Per-provider parity, as policy options (default false = retry under the
|
|
23
|
+
// bounded budget with ordinary backoff):
|
|
24
|
+
// overloadRetryRequiresAdvice — an overload (server_is_overloaded,
|
|
25
|
+
// overloaded_error, 529, statusless overload event) retries ONLY when the
|
|
26
|
+
// server gave advice (Retry-After / retryAfterMs / RetryInfo).
|
|
27
|
+
// generic429RetryRequiresAdvice — a rate limit without a typed
|
|
28
|
+
// rate_limit_exceeded / slow_down code retries ONLY on advice.
|
|
29
|
+
// A typed rate_limit_exceeded / slow_down always retries (advice, else the
|
|
30
|
+
// delay its message names, else local backoff). Returns false when the
|
|
31
|
+
// policy denies the retry, null when it has no say.
|
|
32
|
+
const TYPED_RATE_LIMIT_CODES = new Set(['rate_limit_exceeded', 'slow_down']);
|
|
33
|
+
export function adviceGatedRetry(err, policy = {}) {
|
|
34
|
+
const { kind, code, retryAfterMs } = providerErrorKind(err);
|
|
35
|
+
const advised = retryAfterMs != null;
|
|
36
|
+
if (kind === 'overloaded' && policy.overloadRetryRequiresAdvice === true && !advised) return false;
|
|
37
|
+
if (
|
|
38
|
+
kind === 'rate_limited' &&
|
|
39
|
+
!TYPED_RATE_LIMIT_CODES.has(code) &&
|
|
40
|
+
policy.generic429RetryRequiresAdvice === true &&
|
|
41
|
+
!advised
|
|
42
|
+
) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function retryable429(err, policy) {
|
|
49
|
+
if (policy.retry429 === false) return false;
|
|
50
|
+
if (adviceGatedRetry(err, policy) === false) return false;
|
|
51
|
+
const { kind } = providerErrorKind(err);
|
|
52
|
+
return kind === 'overloaded' || kind === 'rate_limited';
|
|
53
|
+
}
|
|
14
54
|
|
|
15
55
|
// F) Retry-budget profiles as DATA. The numbers live ONLY here now.
|
|
16
56
|
// ws.*Retries (5) — one Responses stream retry budget.
|
|
@@ -88,19 +128,24 @@ function isWebSocketConnectionLimit(err) {
|
|
|
88
128
|
// bucket. Buckets need typed codes; the DEFAULT for an unrecognized (or
|
|
89
129
|
// absent) code is the bounded retry, with fatal refusal codes staying
|
|
90
130
|
// terminal.
|
|
91
|
-
function classifyResponseFailed(failed, attemptIndex, policy) {
|
|
131
|
+
function classifyResponseFailed(failed, attemptIndex, policy, err = null) {
|
|
92
132
|
const detail = failed?.response?.error || failed?.error || failed;
|
|
93
133
|
const failedStatus = typedStatusFrom(detail, failed?.response, failed);
|
|
134
|
+
// Deterministic codes outrank a numeric 5xx on the same payload.
|
|
135
|
+
const keys = [detail?.code, detail?.type]
|
|
136
|
+
.map((field) => (typeof field === 'string' ? field.trim().toLowerCase() : ''))
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
const codeClassifier = keys.map((key) => RESPONSE_FAILED_CODE_CLASSIFIERS.get(key)).find(Boolean) || null;
|
|
139
|
+
if (!codeClassifier && keys.some((key) => WIRE_ERROR_FATAL_CODES.has(key))) return null;
|
|
140
|
+
const subject = { responseFailed: failed, headers: err?.headers, retryAfterMs: err?.retryAfterMs };
|
|
141
|
+
if (DETERMINISTIC_KINDS.has(providerErrorKind(subject).kind)) return null;
|
|
142
|
+
// Statusless overload / generic rate-limit events follow the advice gate.
|
|
143
|
+
if (adviceGatedRetry(subject, policy) === false) return null;
|
|
94
144
|
if (failedStatus >= 500 && failedStatus < 600) {
|
|
95
145
|
if (TERMINAL_EDGE_STATUSES.has(failedStatus)) return null;
|
|
96
146
|
return _allowMidstream(`http_${failedStatus}`, attemptIndex, policy);
|
|
97
147
|
}
|
|
98
|
-
|
|
99
|
-
const key = typeof field === 'string' ? field.trim().toLowerCase() : '';
|
|
100
|
-
const classifier = RESPONSE_FAILED_CODE_CLASSIFIERS.get(key);
|
|
101
|
-
if (classifier) return _allowMidstream(classifier, attemptIndex, policy);
|
|
102
|
-
if (key && WIRE_ERROR_FATAL_CODES.has(key)) return null;
|
|
103
|
-
}
|
|
148
|
+
if (codeClassifier) return _allowMidstream(codeClassifier, attemptIndex, policy);
|
|
104
149
|
// A typed non-transient 4xx on the failure payload is a deterministic
|
|
105
150
|
// refusal even without a recognized code string.
|
|
106
151
|
if (failedStatus >= 400 && failedStatus < 500 && !TRANSIENT_STATUSES.has(failedStatus)) return null;
|
|
@@ -146,12 +191,19 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
146
191
|
// NOMINAL closes count too: a pooled socket the server retires between
|
|
147
192
|
// turns closes with 1000/1001/1005 and must reconnect fresh — treating
|
|
148
193
|
// that as terminal killed the turn instead of reissuing it.
|
|
149
|
-
|
|
194
|
+
// An EXPLICIT wire failure (response.failed / error event) before
|
|
195
|
+
// response.created exposed nothing either: it gets the same bounded WS
|
|
196
|
+
// retry budget, and its typed code decides below.
|
|
197
|
+
const failed = err?.responseFailed || state.responseFailedPayload;
|
|
198
|
+
if (!state.sawResponseCreated && !NON_TERMINAL_STREAM_CLOSE_CODES.has(closeCode) && !failed) return null;
|
|
150
199
|
if (state.userAbort) return null;
|
|
151
200
|
|
|
152
201
|
if (!err) return null;
|
|
153
202
|
const status = Number(err?.httpStatus || 0);
|
|
154
|
-
if (status === 401 || status === 403
|
|
203
|
+
if (status === 401 || status === 403) return null;
|
|
204
|
+
if (DETERMINISTIC_KINDS.has(providerErrorKind(err).kind)) return null;
|
|
205
|
+
if (status === 429) return retryable429(err, policy) ? _allowMidstream('http_429', attemptIndex, policy) : null;
|
|
206
|
+
if (status && adviceGatedRetry(err, policy) === false) return null;
|
|
155
207
|
if (status >= 500 && status < 600) {
|
|
156
208
|
if (TERMINAL_EDGE_STATUSES.has(status)) return null;
|
|
157
209
|
return _allowMidstream(`http_${status}`, attemptIndex, policy);
|
|
@@ -167,8 +219,7 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
167
219
|
return _allowMidstream(`ws_${closeCode}`, attemptIndex, policy);
|
|
168
220
|
}
|
|
169
221
|
|
|
170
|
-
|
|
171
|
-
if (failed) return classifyResponseFailed(failed, attemptIndex, policy);
|
|
222
|
+
if (failed) return classifyResponseFailed(failed, attemptIndex, policy, err);
|
|
172
223
|
return null;
|
|
173
224
|
}
|
|
174
225
|
|
|
@@ -181,8 +232,8 @@ const RESPONSE_FAILED_CODE_CLASSIFIERS = new Map([
|
|
|
181
232
|
['auth_expired', 'response_failed_auth_expired'],
|
|
182
233
|
['previous_response_not_found', 'previous_response_not_found'],
|
|
183
234
|
['websocket_connection_limit_reached', 'websocket_connection_limit'],
|
|
184
|
-
// A rate limit is retried after the server-advised wait
|
|
185
|
-
//
|
|
235
|
+
// A rate limit is retried after the server-advised wait; a usage limit
|
|
236
|
+
// stays a fatal code.
|
|
186
237
|
['rate_limit_exceeded', 'response_failed_rate_limited'],
|
|
187
238
|
]);
|
|
188
239
|
|
|
@@ -197,7 +248,10 @@ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
|
197
248
|
if (!err) return null;
|
|
198
249
|
const status = Number(err?.httpStatus || err?.status || err?.response?.status || 0);
|
|
199
250
|
if (status === 401 || status === 403) return null;
|
|
200
|
-
if (
|
|
251
|
+
if (DETERMINISTIC_KINDS.has(providerErrorKind(err).kind)) return null;
|
|
252
|
+
if (status === 429) return retryable429(err, policy) ? 'http_429' : null;
|
|
253
|
+
// Opt-in: overloads retry only on server advice.
|
|
254
|
+
if (adviceGatedRetry(err, policy) === false) return null;
|
|
201
255
|
if (status >= 500 && status < 600) {
|
|
202
256
|
if (TERMINAL_EDGE_STATUSES.has(status)) return null;
|
|
203
257
|
return `http_${status}`;
|
|
@@ -322,13 +376,24 @@ export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
|
322
376
|
// returns null. HTTP 401 is reserved for auth recovery and 426 for immediate
|
|
323
377
|
// HTTPS fallback. The OpenAI OAuth caller opts out of 429 retries
|
|
324
378
|
// (retry429:false); all other callers retain the historical retryable
|
|
325
|
-
// UnexpectedStatus policy.
|
|
326
|
-
|
|
379
|
+
// UnexpectedStatus policy. overloadRetryRequiresAdvice /
|
|
380
|
+
// generic429RetryRequiresAdvice (default false) opt a caller into
|
|
381
|
+
// advice-gated overload / generic-429 retries.
|
|
382
|
+
export function classifyHandshakeError(
|
|
383
|
+
err,
|
|
384
|
+
{ retry429 = true, overloadRetryRequiresAdvice = false, generic429RetryRequiresAdvice = false } = {}
|
|
385
|
+
) {
|
|
327
386
|
if (!err) return null;
|
|
328
387
|
const code = err.code || '';
|
|
329
388
|
const status = Number(err.httpStatus || 0);
|
|
330
389
|
|
|
331
390
|
if (status === 401 || status === 426 || (status === 429 && !retry429)) return null;
|
|
391
|
+
// Every deterministic refusal (quota, billing, auth, permission, policy,
|
|
392
|
+
// invalid request, context/size) is final, whatever 5xx or advice it carries.
|
|
393
|
+
if (DETERMINISTIC_KINDS.has(providerErrorKind(err).kind)) return null;
|
|
394
|
+
if (status && adviceGatedRetry(err, { overloadRetryRequiresAdvice, generic429RetryRequiresAdvice }) === false) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
332
397
|
if (status > 0) {
|
|
333
398
|
// Typed transient handshake statuses only. A 403/404/4xx upgrade refusal
|
|
334
399
|
// is a deterministic decision: spending the retry budget on it cannot
|
|
@@ -351,6 +351,9 @@ export function normalizeCompactionConfig(value = {}) {
|
|
|
351
351
|
return {
|
|
352
352
|
...next,
|
|
353
353
|
auto: raw.auto !== false && raw.enabled !== false,
|
|
354
|
+
// Compact a conversation on its current model before switching it to
|
|
355
|
+
// another one in place (default on).
|
|
356
|
+
onModelChange: raw.onModelChange !== false,
|
|
354
357
|
};
|
|
355
358
|
}
|
|
356
359
|
|
|
@@ -80,21 +80,3 @@ export function acknowledgePendingGoalReminder(session, revision) {
|
|
|
80
80
|
session.updatedAt = Date.now();
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
export function prependGoalReminderToLatestUserMessage(messages, content) {
|
|
85
|
-
const reminder = clean(content);
|
|
86
|
-
const out = Array.isArray(messages) ? [...messages] : [];
|
|
87
|
-
if (!reminder) return out;
|
|
88
|
-
for (let index = out.length - 1; index >= 0; index -= 1) {
|
|
89
|
-
const message = out[index];
|
|
90
|
-
if (message?.role !== 'user') continue;
|
|
91
|
-
const current = message.content;
|
|
92
|
-
const nextContent = Array.isArray(current)
|
|
93
|
-
? [{ type: 'text', text: `${reminder}\n\n` }, ...current]
|
|
94
|
-
: `${reminder}\n\n${String(current ?? '')}`;
|
|
95
|
-
out[index] = { ...message, content: nextContent };
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
out.push({ role: 'user', content: reminder });
|
|
99
|
-
return out;
|
|
100
|
-
}
|
|
@@ -7,6 +7,7 @@ import { modelSupportsServiceTier } from '../providers/model-service-tiers.mjs';
|
|
|
7
7
|
import { openAiDirectSupportsFast, openAiDirectSupportsUltrafast } from '../providers/openai-direct-request.mjs';
|
|
8
8
|
import { supportsAnthropicFastMode } from '../../../shared/llm/anthropic-betas.mjs';
|
|
9
9
|
import { normalizeContextTokens } from '../../../shared/llm/default-context-window.mjs';
|
|
10
|
+
import { getModelMetadataSync } from '../../../shared/llm/model-catalog.mjs';
|
|
10
11
|
|
|
11
12
|
const FAST_CAPABLE_PROVIDERS = new Set([
|
|
12
13
|
'anthropic',
|
|
@@ -24,41 +25,59 @@ export function routeFastKey(provider, model) {
|
|
|
24
25
|
return p && m ? `${p}/${m}` : '';
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// What one catalog row states about hosted web search: its own
|
|
29
|
+
// `supportsWebSearch` flag (true or false), else a web_search tool in its tool
|
|
30
|
+
// lists. Null when the row says nothing, which leaves the answer to the id
|
|
31
|
+
// fallback of each family.
|
|
32
|
+
function rowWebSearchDeclaration(row) {
|
|
33
|
+
if (!row || typeof row !== 'object') return null;
|
|
34
|
+
if (typeof row.supportsWebSearch === 'boolean') return row.supportsWebSearch;
|
|
31
35
|
const tools = [
|
|
32
|
-
...(Array.isArray(
|
|
33
|
-
...(Array.isArray(
|
|
34
|
-
...(Array.isArray(
|
|
36
|
+
...(Array.isArray(row.supportedTools) ? row.supportedTools : []),
|
|
37
|
+
...(Array.isArray(row.tools) ? row.tools : []),
|
|
38
|
+
...(Array.isArray(row.capabilities?.tools) ? row.capabilities.tools : []),
|
|
35
39
|
].map((tool) => clean(tool?.type || tool?.name || tool).toLowerCase());
|
|
36
|
-
|
|
37
|
-
if (/codex|image|audio|tts|stt|embedding|rerank|moderation|search-preview/.test(id)) return false;
|
|
38
|
-
return /^gpt-(5(?:\.|$|-)|4\.1(?:-|$)|4o(?:-|$)|4\.5(?:-|$))/.test(id) || /^o[34](?:-|$)/.test(id);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function grokModelSupportsHostedWebSearch(model) {
|
|
42
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
43
|
-
if (!id || /imagine|image|video|composer/.test(id)) return false;
|
|
44
|
-
if (id === 'grok-build') return false;
|
|
45
|
-
return /^grok-/.test(id);
|
|
40
|
+
return tools.some((tool) => tool === 'web_search' || tool === 'web_search_preview') ? true : null;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return /^gemini-(3(?:\.|-|$)|2\.5-|2\.0-flash)/.test(id);
|
|
43
|
+
// The route's own row first, then the provider catalog's row for the model.
|
|
44
|
+
function hostedWebSearchDeclared(model, catalogRow) {
|
|
45
|
+
return rowWebSearchDeclaration(model) ?? rowWebSearchDeclaration(catalogRow);
|
|
52
46
|
}
|
|
53
47
|
|
|
54
|
-
|
|
48
|
+
// When neither the route row nor the catalog states hosted web search, the
|
|
49
|
+
// vendor family (already fixed by the provider) is taken as capable unless the
|
|
50
|
+
// id names a non-chat modality or a retired generation. Only those closed,
|
|
51
|
+
// frozen sets are matched by id, so a model released later needs no entry here;
|
|
52
|
+
// guessing "capable" wrongly would hard-fail the request, which is why the
|
|
53
|
+
// retired generations stay excluded.
|
|
54
|
+
function hostedWebSearchFallback(model, catalogRow, unsupportedId) {
|
|
55
55
|
const id = clean(model?.id || model).toLowerCase();
|
|
56
56
|
if (!id) return false;
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
return (
|
|
57
|
+
const declared = hostedWebSearchDeclared(model, catalogRow);
|
|
58
|
+
if (declared !== null) return declared;
|
|
59
|
+
return !unsupportedId.test(id);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const NON_CHAT_ID = 'embedding|rerank|moderation|image|imagen|imagine|video|veo|audio|tts|stt|aqa|search-preview';
|
|
63
|
+
|
|
64
|
+
const OPENAI_UNSUPPORTED = new RegExp(
|
|
65
|
+
`${NON_CHAT_ID}|codex|^gpt-(?:3|4(?:$|-))|^o[12](?:-|$)|^(?:babbage|davinci|text-)`
|
|
66
|
+
);
|
|
67
|
+
const GROK_UNSUPPORTED = new RegExp(`${NON_CHAT_ID}|composer|^grok-build$|^grok-[123](?:[.-]|$)`);
|
|
68
|
+
// The Gemini family is also reached through a relay serving other vendors'
|
|
69
|
+
// models, whose hosted search is Gemini's alone, so a non-Gemini id is out.
|
|
70
|
+
const GEMINI_UNSUPPORTED = new RegExp(
|
|
71
|
+
`${NON_CHAT_ID}|^(?!gemini-)|computer-use|customtools|^gemini-(?:1[.-]|pro|ultra)|^gemini-2\\.0-(?!flash)`
|
|
72
|
+
);
|
|
73
|
+
const ANTHROPIC_UNSUPPORTED = /^claude-(?:\d|instant)|^claude-[a-z]+-[123](?:[-.]|$)/;
|
|
74
|
+
|
|
75
|
+
const openAiModelSupportsHostedWebSearch = (model, row) => hostedWebSearchFallback(model, row, OPENAI_UNSUPPORTED);
|
|
76
|
+
const grokModelSupportsHostedWebSearch = (model, row) => hostedWebSearchFallback(model, row, GROK_UNSUPPORTED);
|
|
77
|
+
const geminiModelSupportsHostedWebSearch = (model, row) => hostedWebSearchFallback(model, row, GEMINI_UNSUPPORTED);
|
|
78
|
+
const anthropicModelSupportsHostedWebSearch = (model, row) =>
|
|
79
|
+
hostedWebSearchFallback(model, row, ANTHROPIC_UNSUPPORTED);
|
|
80
|
+
|
|
62
81
|
export function fastCapableFor(provider, model, effort = null, modelParameters = {}) {
|
|
63
82
|
const p = clean(provider);
|
|
64
83
|
if (!FAST_CAPABLE_PROVIDERS.has(p)) return false;
|
|
@@ -77,8 +96,13 @@ export function fastCapableFor(provider, model, effort = null, modelParameters =
|
|
|
77
96
|
}
|
|
78
97
|
return !selectedEffort || fastEfforts.length === 0 || fastEfforts.includes(selectedEffort);
|
|
79
98
|
}
|
|
80
|
-
if (p === 'openai') return openAiDirectSupportsFast(model);
|
|
81
99
|
if (p === 'openai-oauth') return modelSupportsServiceTier(model, 'priority');
|
|
100
|
+
// The route row's own declaration first, then the catalog via the helpers.
|
|
101
|
+
if (typeof model?.supportsFastMode === 'boolean') return model.supportsFastMode;
|
|
102
|
+
const modelId = clean(model?.id || model);
|
|
103
|
+
const declaredFast = modelId ? getModelMetadataSync(modelId, p)?.supportsFastMode : undefined;
|
|
104
|
+
if (typeof declaredFast === 'boolean') return declaredFast;
|
|
105
|
+
if (p === 'openai') return openAiDirectSupportsFast(model);
|
|
82
106
|
if (p === 'anthropic' || p === 'anthropic-oauth') return supportsAnthropicFastMode(clean(model?.id || model));
|
|
83
107
|
return false;
|
|
84
108
|
}
|
|
@@ -100,10 +124,15 @@ const HOSTED_WEB_SEARCH_BY_FAMILY = {
|
|
|
100
124
|
// webSearchCapableFor needs the web-search family mapping, which lives in
|
|
101
125
|
// workflow.mjs and is pure. Wire it in via a factory to keep this module free
|
|
102
126
|
// of a circular import at load time.
|
|
103
|
-
|
|
127
|
+
// `catalogRowFor(provider, id)` is the provider catalog's row for the model;
|
|
128
|
+
// callers that hold only a bare id (or a row the provider list left silent)
|
|
129
|
+
// still get the catalog's answer before the id fallback applies.
|
|
130
|
+
export function makeWebSearchCapableFor(webSearchProviderFamily, catalogRowFor = getModelMetadataSync) {
|
|
104
131
|
return function webSearchCapableFor(provider, model) {
|
|
105
132
|
const supports = HOSTED_WEB_SEARCH_BY_FAMILY[webSearchProviderFamily(provider)];
|
|
106
|
-
|
|
133
|
+
if (!supports) return false;
|
|
134
|
+
const id = clean(model?.id || model);
|
|
135
|
+
return supports(model, id ? catalogRowFor(id, provider) : null);
|
|
107
136
|
};
|
|
108
137
|
}
|
|
109
138
|
|