@pentoshi/clai 4.1.0 → 4.2.0
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 +14 -0
- package/dist/agent/compaction-executor.d.ts +68 -0
- package/dist/agent/compaction-executor.js +343 -0
- package/dist/agent/compaction-executor.js.map +1 -0
- package/dist/agent/context-breakdown.js +2 -0
- package/dist/agent/context-breakdown.js.map +1 -1
- package/dist/agent/context-manager.d.ts +23 -9
- package/dist/agent/context-manager.js +72 -43
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +3 -0
- package/dist/agent/loop-guard.d.ts +2 -0
- package/dist/agent/loop-guard.js +48 -3
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/prompt-composer.js +6 -6
- package/dist/agent/prompt-composer.js.map +1 -1
- package/dist/agent/reliability-policy.d.ts +1 -0
- package/dist/agent/reliability-policy.js +6 -3
- package/dist/agent/reliability-policy.js.map +1 -1
- package/dist/agent/request-accounting.d.ts +97 -0
- package/dist/agent/request-accounting.js +207 -0
- package/dist/agent/request-accounting.js.map +1 -0
- package/dist/agent/request-budget.d.ts +2 -2
- package/dist/agent/request-budget.js +2 -4
- package/dist/agent/request-budget.js.map +1 -1
- package/dist/agent/runner.d.ts +8 -2
- package/dist/agent/runner.js +409 -178
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-title.d.ts +0 -26
- package/dist/agent/session-title.js +0 -80
- package/dist/agent/session-title.js.map +1 -1
- package/dist/agent/tool-history.d.ts +2 -2
- package/dist/agent/tool-history.js +19 -6
- package/dist/agent/tool-history.js.map +1 -1
- package/dist/app/adapters/agent-event-adapter.js +19 -0
- package/dist/app/adapters/agent-event-adapter.js.map +1 -1
- package/dist/app/adapters/current-agent-adapter.js +4 -0
- package/dist/app/adapters/current-agent-adapter.js.map +1 -1
- package/dist/app/commands/catalog.js +24 -0
- package/dist/app/commands/catalog.js.map +1 -1
- package/dist/app/controllers/cancel-coordinator.d.ts +51 -0
- package/dist/app/controllers/cancel-coordinator.js +48 -0
- package/dist/app/controllers/cancel-coordinator.js.map +1 -0
- package/dist/app/controllers/session-compact-helper.d.ts +19 -2
- package/dist/app/controllers/session-compact-helper.js +100 -127
- package/dist/app/controllers/session-compact-helper.js.map +1 -1
- package/dist/app/controllers/session-context-usage.d.ts +32 -15
- package/dist/app/controllers/session-context-usage.js +186 -51
- package/dist/app/controllers/session-context-usage.js.map +1 -1
- package/dist/app/controllers/session-controller.d.ts +20 -11
- package/dist/app/controllers/session-controller.js +111 -90
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-naming.d.ts +31 -0
- package/dist/app/controllers/session-naming.js +173 -0
- package/dist/app/controllers/session-naming.js.map +1 -0
- package/dist/app/controllers/session-persistence.d.ts +2 -2
- package/dist/app/controllers/session-persistence.js +3 -6
- package/dist/app/controllers/session-persistence.js.map +1 -1
- package/dist/app/controllers/session-turn-request.d.ts +2 -1
- package/dist/app/controllers/session-turn-request.js +3 -0
- package/dist/app/controllers/session-turn-request.js.map +1 -1
- package/dist/app/controllers/turn-controller.d.ts +2 -1
- package/dist/app/controllers/turn-controller.js +1 -0
- package/dist/app/controllers/turn-controller.js.map +1 -1
- package/dist/app/events/app-event.d.ts +9 -0
- package/dist/app/events/app-event.js.map +1 -1
- package/dist/app/ports/agent-port.d.ts +8 -1
- package/dist/classic/app/ClassicApp.js +2 -1
- package/dist/classic/app/ClassicApp.js.map +1 -1
- package/dist/classic/app/app-wiring.js +1 -2
- package/dist/classic/app/app-wiring.js.map +1 -1
- package/dist/classic/app/wiring-interactions.js +2 -7
- package/dist/classic/app/wiring-interactions.js.map +1 -1
- package/dist/classic/blocks/tool-lines.js +2 -1
- package/dist/classic/blocks/tool-lines.js.map +1 -1
- package/dist/classic/input/cancel-ladder.d.ts +2 -19
- package/dist/classic/input/cancel-ladder.js +13 -20
- package/dist/classic/input/cancel-ladder.js.map +1 -1
- package/dist/commands/providers.js +2 -0
- package/dist/commands/providers.js.map +1 -1
- package/dist/llm/adapters/anthropic-tools.d.ts +31 -14
- package/dist/llm/adapters/anthropic-tools.js +124 -29
- package/dist/llm/adapters/anthropic-tools.js.map +1 -1
- package/dist/llm/adapters/gemini-tools.d.ts +16 -4
- package/dist/llm/adapters/gemini-tools.js +117 -16
- package/dist/llm/adapters/gemini-tools.js.map +1 -1
- package/dist/llm/adapters/openai-tools.d.ts +13 -2
- package/dist/llm/adapters/openai-tools.js +58 -11
- package/dist/llm/adapters/openai-tools.js.map +1 -1
- package/dist/llm/agentrouter.js +68 -45
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/anthropic.js +69 -36
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +70 -32
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/bynara.js +26 -69
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/capabilities.d.ts +2 -1
- package/dist/llm/capabilities.js +47 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/context-snapshot.d.ts +100 -0
- package/dist/llm/context-snapshot.js +277 -0
- package/dist/llm/context-snapshot.js.map +1 -0
- package/dist/llm/context-windows.d.ts +10 -0
- package/dist/llm/context-windows.js +129 -0
- package/dist/llm/context-windows.js.map +1 -0
- package/dist/llm/custom-provider-profile.d.ts +75 -0
- package/dist/llm/custom-provider-profile.js +558 -0
- package/dist/llm/custom-provider-profile.js.map +1 -0
- package/dist/llm/custom-providers.d.ts +13 -1
- package/dist/llm/custom-providers.js +77 -25
- package/dist/llm/custom-providers.js.map +1 -1
- package/dist/llm/effort-fallback.d.ts +44 -0
- package/dist/llm/effort-fallback.js +101 -0
- package/dist/llm/effort-fallback.js.map +1 -0
- package/dist/llm/fireworks.js +4 -0
- package/dist/llm/fireworks.js.map +1 -1
- package/dist/llm/free.js +4 -0
- package/dist/llm/free.js.map +1 -1
- package/dist/llm/gemini.d.ts +1 -1
- package/dist/llm/gemini.js +89 -51
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.js +4 -0
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/hetzner.js +4 -0
- package/dist/llm/hetzner.js.map +1 -1
- package/dist/llm/http.d.ts +49 -10
- package/dist/llm/http.js +417 -153
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/kimchi.js +4 -0
- package/dist/llm/kimchi.js.map +1 -1
- package/dist/llm/lightning.js +4 -0
- package/dist/llm/lightning.js.map +1 -1
- package/dist/llm/meta.d.ts +2 -0
- package/dist/llm/meta.js +811 -655
- package/dist/llm/meta.js.map +1 -1
- package/dist/llm/modal.js +4 -0
- package/dist/llm/modal.js.map +1 -1
- package/dist/llm/model-families.d.ts +9 -0
- package/dist/llm/model-families.js +39 -0
- package/dist/llm/model-families.js.map +1 -0
- package/dist/llm/nvidia.js +4 -0
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +44 -40
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openai.js +4 -0
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/openrouter.js +4 -0
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/operation-ledger.d.ts +60 -0
- package/dist/llm/operation-ledger.js +136 -0
- package/dist/llm/operation-ledger.js.map +1 -0
- package/dist/llm/operation-usage.d.ts +32 -0
- package/dist/llm/operation-usage.js +172 -0
- package/dist/llm/operation-usage.js.map +1 -0
- package/dist/llm/orcarouter.d.ts +7 -0
- package/dist/llm/orcarouter.js +166 -0
- package/dist/llm/orcarouter.js.map +1 -0
- package/dist/llm/provider-profile-layers.d.ts +7 -0
- package/dist/llm/provider-profile-layers.js +961 -0
- package/dist/llm/provider-profile-layers.js.map +1 -0
- package/dist/llm/provider-profile.d.ts +181 -0
- package/dist/llm/provider-profile.js +329 -0
- package/dist/llm/provider-profile.js.map +1 -0
- package/dist/llm/provider-profiles.d.ts +13 -0
- package/dist/llm/provider-profiles.js +86 -0
- package/dist/llm/provider-profiles.js.map +1 -0
- package/dist/llm/provider.d.ts +2 -0
- package/dist/llm/provider.js +67 -0
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/qwen-cloud.js +4 -0
- package/dist/llm/qwen-cloud.js.map +1 -1
- package/dist/llm/reasoning-artifacts.d.ts +101 -0
- package/dist/llm/reasoning-artifacts.js +440 -0
- package/dist/llm/reasoning-artifacts.js.map +1 -0
- package/dist/llm/request-fingerprint.d.ts +10 -0
- package/dist/llm/request-fingerprint.js +313 -0
- package/dist/llm/request-fingerprint.js.map +1 -0
- package/dist/llm/request-plan.d.ts +119 -0
- package/dist/llm/request-plan.js +279 -0
- package/dist/llm/request-plan.js.map +1 -0
- package/dist/llm/router.d.ts +19 -1
- package/dist/llm/router.js +447 -75
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/stream-events.d.ts +47 -0
- package/dist/llm/stream-events.js +55 -0
- package/dist/llm/stream-events.js.map +1 -0
- package/dist/llm/stream-terminal.d.ts +30 -0
- package/dist/llm/stream-terminal.js +51 -0
- package/dist/llm/stream-terminal.js.map +1 -0
- package/dist/llm/token-estimate-calibration.d.ts +23 -0
- package/dist/llm/token-estimate-calibration.js +140 -0
- package/dist/llm/token-estimate-calibration.js.map +1 -0
- package/dist/llm/token-usage.d.ts +35 -5
- package/dist/llm/token-usage.js +212 -162
- package/dist/llm/token-usage.js.map +1 -1
- package/dist/llm/tokenrouter.js +8 -2
- package/dist/llm/tokenrouter.js.map +1 -1
- package/dist/modes/ask.js +11 -5
- package/dist/modes/ask.js.map +1 -1
- package/dist/noninteractive/start-noninteractive.js +31 -2
- package/dist/noninteractive/start-noninteractive.js.map +1 -1
- package/dist/noninteractive/stream-blocks.js +15 -4
- package/dist/noninteractive/stream-blocks.js.map +1 -1
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/system.agent.md +6 -0
- package/dist/store/config.js +5 -1
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +2 -0
- package/dist/store/history.js +19 -10
- package/dist/store/history.js.map +1 -1
- package/dist/store/plan.js +20 -14
- package/dist/store/plan.js.map +1 -1
- package/dist/tools/elevated-shell.d.ts +1 -1
- package/dist/tools/elevated-shell.js +24 -72
- package/dist/tools/elevated-shell.js.map +1 -1
- package/dist/tools/nmap-runner.js +30 -36
- package/dist/tools/nmap-runner.js.map +1 -1
- package/dist/tools/sudo-session.d.ts +83 -0
- package/dist/tools/sudo-session.js +124 -0
- package/dist/tools/sudo-session.js.map +1 -0
- package/dist/tui-v2/app/App.js +9 -24
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/components/status/context-limit-chip.js +2 -2
- package/dist/tui-v2/components/status/context-limit-chip.js.map +1 -1
- package/dist/tui-v2/components/transcript/intro-card.js +2 -1
- package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +2 -1
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/types.d.ts +156 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-core/bootstrap/composition-root.d.ts +2 -0
- package/dist/ui-core/bootstrap/composition-root.js +26 -3
- package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
- package/dist/ui-core/commands/picker-commands.js +11 -5
- package/dist/ui-core/commands/picker-commands.js.map +1 -1
- package/dist/ui-core/commands/session-commands.js +41 -5
- package/dist/ui-core/commands/session-commands.js.map +1 -1
- package/dist/ui-core/plan/plan-lifecycle.js +3 -2
- package/dist/ui-core/plan/plan-lifecycle.js.map +1 -1
- package/dist/ui-core/rendering/context-limit.js +4 -1
- package/dist/ui-core/rendering/context-limit.js.map +1 -1
- package/dist/ui-core/rendering/duration.d.ts +2 -1
- package/dist/ui-core/rendering/duration.js +13 -5
- package/dist/ui-core/rendering/duration.js.map +1 -1
- package/dist/ui-core/rendering/intro-header.js +2 -2
- package/dist/ui-core/rendering/intro-header.js.map +1 -1
- package/dist/ui-core/rendering/theme.d.ts +1 -0
- package/dist/ui-core/rendering/theme.js +2 -0
- package/dist/ui-core/rendering/theme.js.map +1 -1
- package/dist/ui-core/state/transcript-hydrate.js +3 -3
- package/dist/ui-core/state/transcript-hydrate.js.map +1 -1
- package/dist/ui-core/state/transcript-reducer.d.ts +5 -0
- package/dist/ui-core/state/transcript-reducer.js +76 -3
- package/dist/ui-core/state/transcript-reducer.js.map +1 -1
- package/dist/ui-core/state/transcript-store.d.ts +10 -1
- package/dist/ui-core/state/transcript-store.js +22 -1
- package/dist/ui-core/state/transcript-store.js.map +1 -1
- package/dist/ui-core/state/transcript-types.d.ts +2 -0
- package/dist/ui-core/state/transcript-types.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +6 -1
package/dist/llm/router.js
CHANGED
|
@@ -3,10 +3,12 @@ import { getProviderKeys, getProviderSecret, markProviderKeySuccess, } from "../
|
|
|
3
3
|
import { anthropicProvider } from "./anthropic.js";
|
|
4
4
|
import { geminiProvider } from "./gemini.js";
|
|
5
5
|
import { groqProvider } from "./groq.js";
|
|
6
|
-
import { ProviderError, bodyAddsInformation, collapseWhitespace, isImageInputUnsupportedError, isReasoningUnsupportedError, stripImagesFromMessages, MAX_ERROR_BODY_IN_MESSAGE_CHARS, STREAM_STALL_MARKER, } from "./http.js";
|
|
7
|
-
import { learnModelVisionCapability, markModelUnavailable, markReasoningUnsupported, modelAcceptsImages, modelSupportsVision, visionSubstitutionOrigin, } from "./capabilities.js";
|
|
6
|
+
import { ProviderError, bodyAddsInformation, buildReasoningPayload, collapseWhitespace, isImageInputUnsupportedError, isReasoningUnsupportedError, stripImagesFromMessages, MAX_ERROR_BODY_IN_MESSAGE_CHARS, STREAM_STALL_MARKER, } from "./http.js";
|
|
7
|
+
import { isReasoningUnsupported, learnModelVisionCapability, markModelUnavailable, markReasoningUnsupported, modelAcceptsImages, modelSupportsVision, visionSubstitutionOrigin, } from "./capabilities.js";
|
|
8
8
|
import { applyImageViewAvailability } from "../prompts/index.js";
|
|
9
|
+
import { fallbackEffortsFor } from "./effort-fallback.js";
|
|
9
10
|
import { markStreamEmittedBytes, streamAlreadyEmitted, streamEmittedBytes, } from "./stream-progress.js";
|
|
11
|
+
import { createStreamEventGuard, isSemanticStreamOutputEvent, } from "./stream-events.js";
|
|
10
12
|
import { attemptsPerKey, buildKeyAttemptPlan, formatKeyEventStatus, isImmediateKeySwitchError, isKeyCircleStopError, isKeyRotatableError, isQuotaKeyError, } from "./key-rotation.js";
|
|
11
13
|
import { freeProvider } from "./free.js";
|
|
12
14
|
import { nvidiaProvider } from "./nvidia.js";
|
|
@@ -24,6 +26,9 @@ import { tokenrouterProvider } from "./tokenrouter.js";
|
|
|
24
26
|
import { metaProvider } from "./meta.js";
|
|
25
27
|
import { fireworksProvider } from "./fireworks.js";
|
|
26
28
|
import { hetznerProvider } from "./hetzner.js";
|
|
29
|
+
import { orcarouterProvider } from "./orcarouter.js";
|
|
30
|
+
import { OperationUsageRecorder, runGenerationAttempt, } from "./operation-usage.js";
|
|
31
|
+
import { isOperationPolicyError, OperationLedger, operationTerminalOutcome, turnOperationPolicy, } from "./operation-ledger.js";
|
|
27
32
|
import { maskSecretTail } from "./provider.js";
|
|
28
33
|
import { getCustomProviderSync } from "./custom-providers.js";
|
|
29
34
|
import { isToolsUnsupportedError, markTextOnlyModel, } from "./tool-protocol.js";
|
|
@@ -32,6 +37,36 @@ const MAX_RETRIES = 6;
|
|
|
32
37
|
const MAX_RETRY_WAIT_MS = 120_000;
|
|
33
38
|
/** Toast replace-key so key rotation never stacks notifications. */
|
|
34
39
|
export const API_KEY_TOAST_KEY = "api-key-rotation";
|
|
40
|
+
/**
|
|
41
|
+
* A budget/policy error raised while recovering from a real failure describes
|
|
42
|
+
* the guard, not the cause. Surface the failure the caller actually needs.
|
|
43
|
+
*/
|
|
44
|
+
function successfulRequestSnapshot(provider, model, request) {
|
|
45
|
+
return structuredClone({
|
|
46
|
+
provider,
|
|
47
|
+
model,
|
|
48
|
+
messages: request.messages,
|
|
49
|
+
...(request.temperature !== undefined
|
|
50
|
+
? { temperature: request.temperature }
|
|
51
|
+
: {}),
|
|
52
|
+
...(request.thinking ? { thinking: request.thinking } : {}),
|
|
53
|
+
...(request.tools ? { tools: request.tools } : {}),
|
|
54
|
+
...(request.toolChoice !== undefined
|
|
55
|
+
? { toolChoice: request.toolChoice }
|
|
56
|
+
: {}),
|
|
57
|
+
...(request.parallelToolCalls !== undefined
|
|
58
|
+
? { parallelToolCalls: request.parallelToolCalls }
|
|
59
|
+
: {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function preservedFailure(recoveryError, originalError) {
|
|
63
|
+
return isOperationPolicyError(recoveryError) ? originalError : recoveryError;
|
|
64
|
+
}
|
|
65
|
+
function resolveOperationLedger(options) {
|
|
66
|
+
if (options.operation)
|
|
67
|
+
return options.operation;
|
|
68
|
+
return new OperationLedger(turnOperationPolicy(), options.attemptUsage ?? new OperationUsageRecorder());
|
|
69
|
+
}
|
|
35
70
|
async function sleep(ms, signal) {
|
|
36
71
|
if (signal?.aborted)
|
|
37
72
|
throw signal.reason ?? new Error("Aborted");
|
|
@@ -56,6 +91,47 @@ async function sleep(ms, signal) {
|
|
|
56
91
|
function isRateLimited(error) {
|
|
57
92
|
return error instanceof ProviderError && error.status === 429;
|
|
58
93
|
}
|
|
94
|
+
function isServerUnavailable(error) {
|
|
95
|
+
if (!(error instanceof ProviderError))
|
|
96
|
+
return false;
|
|
97
|
+
const status = error.status ?? 0;
|
|
98
|
+
return status === 502 || status === 503 || status === 504;
|
|
99
|
+
}
|
|
100
|
+
function isServerError(error) {
|
|
101
|
+
if (!(error instanceof ProviderError))
|
|
102
|
+
return false;
|
|
103
|
+
const status = error.status ?? 0;
|
|
104
|
+
return status >= 500 && status <= 504;
|
|
105
|
+
}
|
|
106
|
+
function isReasoningRelatedServerError(error, providerId) {
|
|
107
|
+
if (!isServerError(error))
|
|
108
|
+
return false;
|
|
109
|
+
if (providerId === "tokenrouter")
|
|
110
|
+
return true;
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
const body = error instanceof ProviderError ? (error.body ?? "") : "";
|
|
113
|
+
return /reasoning|effort|thinking/i.test(`${message} ${body}`);
|
|
114
|
+
}
|
|
115
|
+
function shouldContinueEffortLadder(error, providerId) {
|
|
116
|
+
return (isReasoningUnsupportedError(error) ||
|
|
117
|
+
isReasoningRelatedServerError(error, providerId));
|
|
118
|
+
}
|
|
119
|
+
function shouldEnterEffortLadder(error, thinking, providerId, model, singleDispatch) {
|
|
120
|
+
if (isReasoningUnsupportedError(error))
|
|
121
|
+
return true;
|
|
122
|
+
if (singleDispatch)
|
|
123
|
+
return false;
|
|
124
|
+
if (!thinking?.enabled)
|
|
125
|
+
return false;
|
|
126
|
+
if (isReasoningUnsupported(providerId, model))
|
|
127
|
+
return false;
|
|
128
|
+
return isReasoningRelatedServerError(error, providerId);
|
|
129
|
+
}
|
|
130
|
+
function isCacheOnlyColdError(error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
const body = error instanceof ProviderError ? (error.body ?? "") : "";
|
|
133
|
+
return /cache_only_cold|cache-only admission/i.test(`${message} ${body}`);
|
|
134
|
+
}
|
|
59
135
|
function isTransientNetworkError(error) {
|
|
60
136
|
const message = error instanceof Error ? error.message : String(error);
|
|
61
137
|
const msg = message.toLowerCase();
|
|
@@ -166,6 +242,9 @@ export function formatProviderFailureForUser(error) {
|
|
|
166
242
|
return withFullBody(withExactError("Provider rejected the request body (422). Model name or parameters may be incompatible — try another model."));
|
|
167
243
|
}
|
|
168
244
|
if (status === 503 || status === 502 || status === 504) {
|
|
245
|
+
if (isCacheOnlyColdError(error)) {
|
|
246
|
+
return withFullBody(withExactError(`Gateway cache admission rejected (${status}; cache_only_cold): the route is cold or overloaded and retried automatically with backoff. If it persists, retry shortly or switch provider/model.`));
|
|
247
|
+
}
|
|
169
248
|
return withFullBody(withExactError(`Upstream provider unavailable (${status}). Retry shortly or switch provider/model; free-tier models are often capacity-constrained.`));
|
|
170
249
|
}
|
|
171
250
|
if (status >= 500 && status < 600) {
|
|
@@ -290,6 +369,7 @@ export const providers = {
|
|
|
290
369
|
meta: metaProvider,
|
|
291
370
|
fireworks: fireworksProvider,
|
|
292
371
|
hetzner: hetznerProvider,
|
|
372
|
+
orcarouter: orcarouterProvider,
|
|
293
373
|
};
|
|
294
374
|
const fallbackOrder = [
|
|
295
375
|
"free",
|
|
@@ -311,6 +391,7 @@ const fallbackOrder = [
|
|
|
311
391
|
"meta",
|
|
312
392
|
"fireworks",
|
|
313
393
|
"hetzner",
|
|
394
|
+
"orcarouter",
|
|
314
395
|
];
|
|
315
396
|
/**
|
|
316
397
|
* Built-in provider ids in fallback preference order. Custom (user-defined)
|
|
@@ -387,6 +468,9 @@ function authForSlot(providerId, value) {
|
|
|
387
468
|
function withoutReasoning(request) {
|
|
388
469
|
return { ...request, thinking: undefined };
|
|
389
470
|
}
|
|
471
|
+
function reasoningWireKey(thinking, style, model, providerId) {
|
|
472
|
+
return JSON.stringify(buildReasoningPayload(thinking, style, model, providerId));
|
|
473
|
+
}
|
|
390
474
|
function makeKeyEmitter(onStatus, onKeyEvent) {
|
|
391
475
|
return (event) => {
|
|
392
476
|
onKeyEvent?.(event);
|
|
@@ -400,39 +484,102 @@ function makeKeyEmitter(onStatus, onKeyEvent) {
|
|
|
400
484
|
}
|
|
401
485
|
};
|
|
402
486
|
}
|
|
403
|
-
|
|
487
|
+
const SELF_RECORDED_PROVIDERS = new Set([
|
|
488
|
+
"agentrouter",
|
|
489
|
+
"bynara",
|
|
490
|
+
"meta",
|
|
491
|
+
]);
|
|
492
|
+
async function runRecordedProviderAttempt(input) {
|
|
493
|
+
if (SELF_RECORDED_PROVIDERS.has(input.providerId))
|
|
494
|
+
return input.run();
|
|
495
|
+
return runGenerationAttempt(input.request, {
|
|
496
|
+
provider: input.providerId,
|
|
497
|
+
model: input.model,
|
|
498
|
+
mode: input.mode,
|
|
499
|
+
reason: input.reason,
|
|
500
|
+
}, input.run);
|
|
501
|
+
}
|
|
502
|
+
async function tryCompleteOnce(provider, providerId, request, model, auth, reason, onStatus, singleDispatch = false) {
|
|
404
503
|
const activeRequest = { ...request, provider: providerId, model };
|
|
504
|
+
const runAttempt = (candidate, attemptReason) => {
|
|
505
|
+
const attemptRequest = { ...candidate, attemptReason };
|
|
506
|
+
return runRecordedProviderAttempt({
|
|
507
|
+
providerId,
|
|
508
|
+
model: attemptRequest.model ?? model,
|
|
509
|
+
mode: "complete",
|
|
510
|
+
reason: attemptReason,
|
|
511
|
+
request: attemptRequest,
|
|
512
|
+
run: () => provider.complete(attemptRequest, auth),
|
|
513
|
+
});
|
|
514
|
+
};
|
|
405
515
|
try {
|
|
406
|
-
const result = await
|
|
516
|
+
const result = await runAttempt(activeRequest, reason);
|
|
407
517
|
if (hasImageInput(activeRequest)) {
|
|
408
518
|
learnModelVisionCapability(providerId, model, true);
|
|
409
519
|
}
|
|
410
520
|
return result;
|
|
411
521
|
}
|
|
412
522
|
catch (error) {
|
|
523
|
+
// A pinned single-dispatch operation records the capability verdict for the
|
|
524
|
+
// next operation but never spends a second physical request on it.
|
|
413
525
|
if (activeRequest.tools?.length && isToolsUnsupportedError(error)) {
|
|
414
526
|
markTextOnlyModel(providerId, model);
|
|
527
|
+
if (singleDispatch)
|
|
528
|
+
throw error;
|
|
415
529
|
const textRequest = {
|
|
416
530
|
...activeRequest,
|
|
417
531
|
tools: undefined,
|
|
418
532
|
toolChoice: undefined,
|
|
419
533
|
parallelToolCalls: undefined,
|
|
420
534
|
};
|
|
421
|
-
return await
|
|
535
|
+
return await runAttempt(textRequest, "adaptation");
|
|
422
536
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
537
|
+
if (shouldEnterEffortLadder(error, activeRequest.thinking, providerId, model, singleDispatch)) {
|
|
538
|
+
if (singleDispatch) {
|
|
539
|
+
markReasoningUnsupported(providerId, model);
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
const thinking = activeRequest.thinking;
|
|
543
|
+
if (thinking?.enabled) {
|
|
544
|
+
const style = provider.reasoningStyle ?? "none";
|
|
545
|
+
const seen = new Set([
|
|
546
|
+
reasoningWireKey(thinking, style, model, providerId),
|
|
547
|
+
]);
|
|
548
|
+
for (const effort of fallbackEffortsFor(thinking.effort)) {
|
|
549
|
+
const candidate = { ...thinking, effort };
|
|
550
|
+
const key = reasoningWireKey(candidate, style, model, providerId);
|
|
551
|
+
if (seen.has(key))
|
|
552
|
+
continue;
|
|
553
|
+
seen.add(key);
|
|
554
|
+
onStatus?.(`ℹ ${providerId}/${model} rejected reasoning effort — retrying with ${effort}`);
|
|
555
|
+
const retryRequest = {
|
|
556
|
+
...activeRequest,
|
|
557
|
+
thinking: candidate,
|
|
558
|
+
};
|
|
559
|
+
try {
|
|
560
|
+
return await runAttempt(retryRequest, "adaptation");
|
|
561
|
+
}
|
|
562
|
+
catch (retryError) {
|
|
563
|
+
if (!shouldContinueEffortLadder(retryError, providerId))
|
|
564
|
+
throw retryError;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
426
568
|
markReasoningUnsupported(providerId, model);
|
|
427
|
-
|
|
569
|
+
onStatus?.(`ℹ ${providerId}/${model} rejected reasoning options — retrying without them`);
|
|
570
|
+
return await runAttempt(withoutReasoning(activeRequest), "adaptation");
|
|
428
571
|
}
|
|
429
572
|
if (hasImageInput(activeRequest) && isImageInputUnsupportedError(error)) {
|
|
430
573
|
learnModelVisionCapability(providerId, model, false);
|
|
431
|
-
|
|
574
|
+
if (singleDispatch)
|
|
575
|
+
throw error;
|
|
576
|
+
return await runAttempt(withoutImages(activeRequest), "adaptation");
|
|
432
577
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
578
|
+
if (!singleDispatch) {
|
|
579
|
+
const restored = revertVisionSubstitution(providerId, model, activeRequest, error);
|
|
580
|
+
if (restored) {
|
|
581
|
+
return await runAttempt(restored.request, "adaptation");
|
|
582
|
+
}
|
|
436
583
|
}
|
|
437
584
|
throw error;
|
|
438
585
|
}
|
|
@@ -502,27 +649,57 @@ function revertVisionSubstitution(providerId, model, request, error) {
|
|
|
502
649
|
request: requestForRoute(restoredRequest, providerId, original),
|
|
503
650
|
};
|
|
504
651
|
}
|
|
505
|
-
async function tryStreamOnce(provider, providerId, request, model, auth, onToken, onStatus) {
|
|
652
|
+
async function tryStreamOnce(provider, providerId, request, model, auth, onToken, onStatus, reason, singleDispatch = false, onSuccessfulRequest) {
|
|
506
653
|
let emittedBytes = 0;
|
|
507
654
|
let emittedToolArgumentBytes = 0;
|
|
508
655
|
const onToolCallDelta = request.onToolCallDelta;
|
|
656
|
+
const downstreamEvents = request.onStreamEvent;
|
|
657
|
+
let guard = createStreamEventGuard();
|
|
658
|
+
const startedToolCallIndexes = new Set();
|
|
659
|
+
const emitEvent = (event) => {
|
|
660
|
+
guard.accept(event);
|
|
661
|
+
if (event.type === "reasoning_delta" || event.type === "commentary_delta") {
|
|
662
|
+
emittedBytes += event.text.length;
|
|
663
|
+
}
|
|
664
|
+
downstreamEvents?.(event);
|
|
665
|
+
};
|
|
509
666
|
const activeRequest = {
|
|
510
667
|
...request,
|
|
511
668
|
provider: providerId,
|
|
512
669
|
model,
|
|
513
|
-
...(onToolCallDelta
|
|
670
|
+
...(onToolCallDelta || downstreamEvents
|
|
514
671
|
? {
|
|
515
672
|
onToolCallDelta: (delta) => {
|
|
516
673
|
const argumentBytes = delta.argumentsBytes ?? 0;
|
|
517
674
|
emittedBytes += Math.max(delta.name?.length ?? 0, argumentBytes - emittedToolArgumentBytes, 1);
|
|
518
675
|
emittedToolArgumentBytes = Math.max(emittedToolArgumentBytes, argumentBytes);
|
|
519
|
-
|
|
676
|
+
if (delta.name !== undefined &&
|
|
677
|
+
!startedToolCallIndexes.has(delta.index)) {
|
|
678
|
+
startedToolCallIndexes.add(delta.index);
|
|
679
|
+
emitEvent({
|
|
680
|
+
type: "tool_call_started",
|
|
681
|
+
index: delta.index,
|
|
682
|
+
...(delta.id ? { id: delta.id } : {}),
|
|
683
|
+
name: delta.name,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
emitEvent({
|
|
687
|
+
type: "tool_arguments_delta",
|
|
688
|
+
index: delta.index,
|
|
689
|
+
...(delta.id ? { id: delta.id } : {}),
|
|
690
|
+
argumentsBytes: argumentBytes,
|
|
691
|
+
});
|
|
692
|
+
onToolCallDelta?.(delta);
|
|
520
693
|
},
|
|
521
694
|
}
|
|
522
695
|
: {}),
|
|
696
|
+
...(downstreamEvents ? { onStreamEvent: emitEvent } : {}),
|
|
523
697
|
};
|
|
524
698
|
const emit = (token) => {
|
|
699
|
+
if (!token)
|
|
700
|
+
return;
|
|
525
701
|
emittedBytes += token.length;
|
|
702
|
+
emitEvent({ type: "answer_delta", text: token });
|
|
526
703
|
onToken(token);
|
|
527
704
|
};
|
|
528
705
|
const learnVisionOnSuccess = () => {
|
|
@@ -530,14 +707,62 @@ async function tryStreamOnce(provider, providerId, request, model, auth, onToken
|
|
|
530
707
|
learnModelVisionCapability(providerId, model, true);
|
|
531
708
|
}
|
|
532
709
|
};
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
710
|
+
const runAttempt = async (candidate, attemptReason) => {
|
|
711
|
+
guard = createStreamEventGuard();
|
|
712
|
+
startedToolCallIndexes.clear();
|
|
713
|
+
const attemptRequest = { ...candidate, attemptReason };
|
|
714
|
+
const result = await runRecordedProviderAttempt({
|
|
715
|
+
providerId,
|
|
716
|
+
model: attemptRequest.model ?? model,
|
|
717
|
+
mode: "stream",
|
|
718
|
+
reason: attemptReason,
|
|
719
|
+
request: attemptRequest,
|
|
720
|
+
run: async () => {
|
|
721
|
+
let result;
|
|
722
|
+
if (provider.stream) {
|
|
723
|
+
result = await provider.stream(attemptRequest, auth, emit);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
result = await provider.complete(attemptRequest, auth);
|
|
727
|
+
emit(result.text);
|
|
728
|
+
}
|
|
729
|
+
for (const [index, call] of (result.toolCalls ?? []).entries()) {
|
|
730
|
+
if (!startedToolCallIndexes.has(index)) {
|
|
731
|
+
startedToolCallIndexes.add(index);
|
|
732
|
+
emitEvent({
|
|
733
|
+
type: "tool_call_started",
|
|
734
|
+
index,
|
|
735
|
+
...(call.id ? { id: call.id } : {}),
|
|
736
|
+
name: call.name,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
emitEvent({
|
|
740
|
+
type: "tool_call_completed",
|
|
741
|
+
index,
|
|
742
|
+
...(call.id ? { id: call.id } : {}),
|
|
743
|
+
name: call.name,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
if (result.usage) {
|
|
747
|
+
emitEvent({ type: "usage_observed", usage: result.usage });
|
|
748
|
+
}
|
|
749
|
+
emitEvent({
|
|
750
|
+
type: "provider_terminal",
|
|
751
|
+
...(result.finishReason
|
|
752
|
+
? { finishReason: result.finishReason }
|
|
753
|
+
: {}),
|
|
754
|
+
});
|
|
755
|
+
return result;
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
try {
|
|
759
|
+
onSuccessfulRequest?.(successfulRequestSnapshot(result.provider || providerId, result.model || attemptRequest.model || model, attemptRequest));
|
|
538
760
|
}
|
|
539
|
-
|
|
540
|
-
|
|
761
|
+
catch { }
|
|
762
|
+
return result;
|
|
763
|
+
};
|
|
764
|
+
try {
|
|
765
|
+
const result = await runAttempt(activeRequest, reason);
|
|
541
766
|
learnVisionOnSuccess();
|
|
542
767
|
return result;
|
|
543
768
|
}
|
|
@@ -546,6 +771,8 @@ async function tryStreamOnce(provider, providerId, request, model, auth, onToken
|
|
|
546
771
|
activeRequest.tools?.length &&
|
|
547
772
|
isToolsUnsupportedError(error)) {
|
|
548
773
|
markTextOnlyModel(providerId, model);
|
|
774
|
+
if (singleDispatch)
|
|
775
|
+
throw markStreamEmittedBytes(error, emittedBytes);
|
|
549
776
|
onStatus?.(`ℹ ${providerId}/${model} does not support native tools — falling back to text protocol`);
|
|
550
777
|
const textRequest = {
|
|
551
778
|
...activeRequest,
|
|
@@ -554,69 +781,84 @@ async function tryStreamOnce(provider, providerId, request, model, auth, onToken
|
|
|
554
781
|
parallelToolCalls: undefined,
|
|
555
782
|
};
|
|
556
783
|
try {
|
|
557
|
-
|
|
558
|
-
return await provider.stream(textRequest, auth, emit);
|
|
559
|
-
}
|
|
560
|
-
const result = await provider.complete(textRequest, auth);
|
|
561
|
-
emit(result.text);
|
|
562
|
-
return result;
|
|
784
|
+
return await runAttempt(textRequest, "adaptation");
|
|
563
785
|
}
|
|
564
786
|
catch (retryError) {
|
|
565
|
-
throw markStreamEmittedBytes(retryError, emittedBytes);
|
|
787
|
+
throw markStreamEmittedBytes(preservedFailure(retryError, error), emittedBytes);
|
|
566
788
|
}
|
|
567
789
|
}
|
|
568
790
|
// Model rejected a reasoning/thinking knob (e.g. chat_template_kwargs on a
|
|
569
|
-
// NIM chat template that does not accept it).
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
|
|
791
|
+
// NIM chat template that does not accept it). A parameter rejection is a
|
|
792
|
+
// request-time 4xx, so no tokens have streamed yet — retries are clean.
|
|
793
|
+
// Walk down the effort ladder first (max → xhigh → high → medium → low) so
|
|
794
|
+
// a model that merely rejects the highest requested depth keeps reasoning;
|
|
795
|
+
// only strip reasoning entirely once every candidate has been rejected.
|
|
796
|
+
if (emittedBytes === 0 && shouldEnterEffortLadder(error, activeRequest.thinking, providerId, model, singleDispatch)) {
|
|
797
|
+
if (singleDispatch) {
|
|
798
|
+
markReasoningUnsupported(providerId, model);
|
|
799
|
+
throw markStreamEmittedBytes(error, emittedBytes);
|
|
800
|
+
}
|
|
801
|
+
const thinking = activeRequest.thinking;
|
|
802
|
+
if (thinking?.enabled) {
|
|
803
|
+
const style = provider.reasoningStyle ?? "none";
|
|
804
|
+
const seen = new Set([
|
|
805
|
+
reasoningWireKey(thinking, style, model, providerId),
|
|
806
|
+
]);
|
|
807
|
+
for (const effort of fallbackEffortsFor(thinking.effort)) {
|
|
808
|
+
const candidate = { ...thinking, effort };
|
|
809
|
+
const key = reasoningWireKey(candidate, style, model, providerId);
|
|
810
|
+
if (seen.has(key))
|
|
811
|
+
continue;
|
|
812
|
+
seen.add(key);
|
|
813
|
+
onStatus?.(`ℹ ${providerId}/${model} rejected reasoning effort — retrying with ${effort}`);
|
|
814
|
+
const retryRequest = {
|
|
815
|
+
...activeRequest,
|
|
816
|
+
thinking: candidate,
|
|
817
|
+
};
|
|
818
|
+
try {
|
|
819
|
+
return await runAttempt(retryRequest, "adaptation");
|
|
820
|
+
}
|
|
821
|
+
catch (retryError) {
|
|
822
|
+
if (!shouldContinueEffortLadder(retryError, providerId)) {
|
|
823
|
+
throw markStreamEmittedBytes(preservedFailure(retryError, error), emittedBytes);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
573
828
|
markReasoningUnsupported(providerId, model);
|
|
574
829
|
onStatus?.(`ℹ ${providerId}/${model} rejected reasoning options — retrying without them`);
|
|
575
830
|
const retryRequest = withoutReasoning(activeRequest);
|
|
576
831
|
try {
|
|
577
|
-
|
|
578
|
-
return await provider.stream(retryRequest, auth, emit);
|
|
579
|
-
}
|
|
580
|
-
const result = await provider.complete(retryRequest, auth);
|
|
581
|
-
emit(result.text);
|
|
582
|
-
return result;
|
|
832
|
+
return await runAttempt(retryRequest, "adaptation");
|
|
583
833
|
}
|
|
584
834
|
catch (retryError) {
|
|
585
|
-
throw markStreamEmittedBytes(retryError, emittedBytes);
|
|
835
|
+
throw markStreamEmittedBytes(preservedFailure(retryError, error), emittedBytes);
|
|
586
836
|
}
|
|
587
837
|
}
|
|
588
838
|
if (emittedBytes === 0 &&
|
|
589
839
|
hasImageInput(activeRequest) &&
|
|
590
840
|
isImageInputUnsupportedError(error)) {
|
|
591
841
|
learnModelVisionCapability(providerId, model, false);
|
|
842
|
+
if (singleDispatch)
|
|
843
|
+
throw markStreamEmittedBytes(error, emittedBytes);
|
|
592
844
|
onStatus?.(`ℹ ${providerId}/${model} rejected image input — retrying without the attached image(s)`);
|
|
593
845
|
const textOnlyRequest = withoutImages(activeRequest);
|
|
594
846
|
try {
|
|
595
|
-
|
|
596
|
-
return await provider.stream(textOnlyRequest, auth, emit);
|
|
597
|
-
}
|
|
598
|
-
const result = await provider.complete(textOnlyRequest, auth);
|
|
599
|
-
emit(result.text);
|
|
600
|
-
return result;
|
|
847
|
+
return await runAttempt(textOnlyRequest, "adaptation");
|
|
601
848
|
}
|
|
602
849
|
catch (retryError) {
|
|
603
|
-
throw markStreamEmittedBytes(retryError, emittedBytes);
|
|
850
|
+
throw markStreamEmittedBytes(preservedFailure(retryError, error), emittedBytes);
|
|
604
851
|
}
|
|
605
852
|
}
|
|
606
|
-
if (emittedBytes === 0) {
|
|
853
|
+
if (emittedBytes === 0 && !singleDispatch) {
|
|
607
854
|
const restored = revertVisionSubstitution(providerId, model, activeRequest, error);
|
|
608
855
|
if (restored) {
|
|
609
856
|
onStatus?.(`ℹ ${providerId}/${model} is not available on this account — falling back to ${restored.original}`);
|
|
610
857
|
try {
|
|
611
|
-
|
|
612
|
-
return await provider.stream(restored.request, auth, emit);
|
|
613
|
-
}
|
|
614
|
-
const result = await provider.complete(restored.request, auth);
|
|
615
|
-
emit(result.text);
|
|
616
|
-
return result;
|
|
858
|
+
return await runAttempt(restored.request, "adaptation");
|
|
617
859
|
}
|
|
618
860
|
catch (retryError) {
|
|
619
|
-
throw markStreamEmittedBytes(retryError, emittedBytes);
|
|
861
|
+
throw markStreamEmittedBytes(preservedFailure(retryError, error), emittedBytes);
|
|
620
862
|
}
|
|
621
863
|
}
|
|
622
864
|
}
|
|
@@ -630,12 +872,16 @@ async function tryStreamOnce(provider, providerId, request, model, auth, onToken
|
|
|
630
872
|
*/
|
|
631
873
|
async function runWithKeyRotation(opts) {
|
|
632
874
|
const { providerId, provider, request, model, emitKey } = opts;
|
|
875
|
+
const singleDispatch = opts.singleDispatch === true;
|
|
633
876
|
const multi = await getProviderKeys(providerId);
|
|
634
877
|
const slots = multi.keys;
|
|
635
878
|
if (slots.length === 0) {
|
|
636
879
|
throw new Error("no API key configured");
|
|
637
880
|
}
|
|
638
|
-
const
|
|
881
|
+
const fullPlan = buildKeyAttemptPlan(slots.length, multi.activeIndex).filter((index) => !slots[index].disabled);
|
|
882
|
+
// A pinned operation uses the sticky key only: rotating would send the same
|
|
883
|
+
// large prompt again under a different credential.
|
|
884
|
+
const plan = singleDispatch ? fullPlan.slice(0, 1) : fullPlan;
|
|
639
885
|
if (plan.length === 0) {
|
|
640
886
|
throw new Error(`all ${slots.length} API key${slots.length === 1 ? "" : "s"} for ${providerId} are disabled — re-enable one to use this provider`);
|
|
641
887
|
}
|
|
@@ -643,12 +889,14 @@ async function runWithKeyRotation(opts) {
|
|
|
643
889
|
const multiKey = enabledCount > 1;
|
|
644
890
|
// Single key: time-increasing retries (MAX_RETRIES+1 attempts).
|
|
645
891
|
// Multi key: 2 attempts per key (initial + one retry), then next key.
|
|
646
|
-
const maxPerKey =
|
|
892
|
+
const maxPerKey = singleDispatch
|
|
893
|
+
? 1
|
|
894
|
+
: attemptsPerKey(enabledCount, (opts.maxRetries ?? MAX_RETRIES) + 1);
|
|
647
895
|
let lastError;
|
|
648
896
|
// Endpoint failover: a provider can carry several base URLs (e.g. Modal
|
|
649
897
|
// workspaces). On an auth/quota error the active endpoint may simply be the
|
|
650
898
|
// wrong workspace for the key, so rotate the endpoint too before giving up.
|
|
651
|
-
const storedEndpoints = providerUsesEndpoints(providerId)
|
|
899
|
+
const storedEndpoints = providerUsesEndpoints(providerId) && !singleDispatch
|
|
652
900
|
? getProviderEndpoints(providerId)
|
|
653
901
|
: undefined;
|
|
654
902
|
const endpointUrls = (storedEndpoints?.urls ?? []).filter((url) => !(storedEndpoints?.disabledUrls ?? []).includes(url));
|
|
@@ -684,13 +932,14 @@ async function runWithKeyRotation(opts) {
|
|
|
684
932
|
}
|
|
685
933
|
for (let attempt = 0; attempt < maxPerKey; attempt++) {
|
|
686
934
|
request.signal?.throwIfAborted();
|
|
935
|
+
const attemptReason = planIdx === 0 && attempt === 0 ? opts.initialAttemptReason : "retry";
|
|
687
936
|
try {
|
|
688
937
|
let result;
|
|
689
938
|
if (opts.mode === "stream") {
|
|
690
|
-
result = await tryStreamOnce(provider, providerId, request, model, auth, opts.onToken ?? (() => { }), opts.onStatus);
|
|
939
|
+
result = await tryStreamOnce(provider, providerId, request, model, auth, opts.onToken ?? (() => { }), opts.onStatus, attemptReason, singleDispatch, opts.onSuccessfulRequest);
|
|
691
940
|
}
|
|
692
941
|
else {
|
|
693
|
-
result = await tryCompleteOnce(provider, providerId, request, model, auth);
|
|
942
|
+
result = await tryCompleteOnce(provider, providerId, request, model, auth, attemptReason, opts.onStatus, singleDispatch);
|
|
694
943
|
}
|
|
695
944
|
// Sticky success only for stored multi-key (not env-only synthetic).
|
|
696
945
|
if (multi.source !== "env" && multi.source !== "local") {
|
|
@@ -712,7 +961,13 @@ async function runWithKeyRotation(opts) {
|
|
|
712
961
|
return result;
|
|
713
962
|
}
|
|
714
963
|
catch (error) {
|
|
964
|
+
const previousError = lastError;
|
|
715
965
|
lastError = error;
|
|
966
|
+
// An admission guard that fires while recovering from a real failure
|
|
967
|
+
// hides that failure. Report the cause the user can act on.
|
|
968
|
+
if (isOperationPolicyError(error) && previousError !== undefined) {
|
|
969
|
+
throw previousError;
|
|
970
|
+
}
|
|
716
971
|
// 404/422: other keys for the same model will not help.
|
|
717
972
|
if (isKeyCircleStopError(error)) {
|
|
718
973
|
throw error;
|
|
@@ -746,7 +1001,7 @@ async function runWithKeyRotation(opts) {
|
|
|
746
1001
|
}
|
|
747
1002
|
const canRetrySame = attempt + 1 < maxPerKey;
|
|
748
1003
|
if (canRetrySame) {
|
|
749
|
-
const wait = isRateLimited(error)
|
|
1004
|
+
const wait = isRateLimited(error) || isServerUnavailable(error)
|
|
750
1005
|
? retryWaitMs(error, attempt)
|
|
751
1006
|
: networkRetryWaitMs(attempt);
|
|
752
1007
|
if (wait > MAX_RETRY_WAIT_MS) {
|
|
@@ -807,15 +1062,19 @@ async function runWithKeyRotation(opts) {
|
|
|
807
1062
|
const err = lastError ?? new Error("all API keys failed");
|
|
808
1063
|
throw err;
|
|
809
1064
|
}
|
|
810
|
-
|
|
1065
|
+
async function completeWithProviderOperation(request, options, ledger) {
|
|
811
1066
|
const config = getConfig();
|
|
1067
|
+
const singleDispatch = options.singleDispatch === true;
|
|
812
1068
|
const requested = request.provider ?? config.defaultProvider;
|
|
813
1069
|
const providerImpl = getProvider(requested);
|
|
814
1070
|
const isDefaultModel = !request.model || request.model === providerImpl.defaultModel;
|
|
815
|
-
const fallbackEnabled =
|
|
1071
|
+
const fallbackEnabled = !singleDispatch &&
|
|
1072
|
+
config.providerFallback &&
|
|
816
1073
|
(await requestedRealKeyCount(requested)) !== 1 &&
|
|
817
1074
|
(isDefaultModel || request.allowModelFallback === true);
|
|
818
|
-
const order =
|
|
1075
|
+
const order = singleDispatch
|
|
1076
|
+
? [requested]
|
|
1077
|
+
: buildFallbackChain(requested, config.freeOnly, fallbackEnabled, request.preferModelFallback === true);
|
|
819
1078
|
const failures = [];
|
|
820
1079
|
const emitKey = makeKeyEmitter(options?.onStatus, options?.onKeyEvent);
|
|
821
1080
|
for (const providerId of order) {
|
|
@@ -830,7 +1089,10 @@ export async function completeWithProvider(request, options) {
|
|
|
830
1089
|
const model = providerId === requested
|
|
831
1090
|
? (request.model ?? provider.defaultModel)
|
|
832
1091
|
: provider.defaultModel;
|
|
833
|
-
const routeRequest =
|
|
1092
|
+
const routeRequest = {
|
|
1093
|
+
...requestForRoute(request, providerId, model),
|
|
1094
|
+
attemptUsage: ledger,
|
|
1095
|
+
};
|
|
834
1096
|
try {
|
|
835
1097
|
const result = await runWithKeyRotation({
|
|
836
1098
|
providerId,
|
|
@@ -839,17 +1101,26 @@ export async function completeWithProvider(request, options) {
|
|
|
839
1101
|
model,
|
|
840
1102
|
emitKey,
|
|
841
1103
|
mode: "complete",
|
|
1104
|
+
initialAttemptReason: providerId === requested ? "initial" : "fallback",
|
|
842
1105
|
...(options?.onStatus ? { onStatus: options.onStatus } : {}),
|
|
843
1106
|
...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
|
1107
|
+
...(singleDispatch ? { singleDispatch: true } : {}),
|
|
844
1108
|
});
|
|
845
1109
|
if (providerId !== requested) {
|
|
846
|
-
|
|
847
|
-
|
|
1110
|
+
if (options?.adoptFallback === true) {
|
|
1111
|
+
setDefaultProvider(providerId);
|
|
1112
|
+
setProviderModel(providerId, result.model || model);
|
|
1113
|
+
}
|
|
848
1114
|
options?.onStatus?.(`switching to ${providerId}/${result.model || model} after ${requested} failed`);
|
|
849
1115
|
}
|
|
850
1116
|
return result;
|
|
851
1117
|
}
|
|
852
1118
|
catch (error) {
|
|
1119
|
+
if (isOperationPolicyError(error)) {
|
|
1120
|
+
if (failures.length === 0)
|
|
1121
|
+
throw error;
|
|
1122
|
+
throw aggregateProviderError(`No provider could complete the request.${formatFailures(failures)}`, failures);
|
|
1123
|
+
}
|
|
853
1124
|
failures.push({
|
|
854
1125
|
provider: providerId,
|
|
855
1126
|
message: failureMessageFor(providerId, error),
|
|
@@ -863,18 +1134,68 @@ export async function completeWithProvider(request, options) {
|
|
|
863
1134
|
}
|
|
864
1135
|
throw aggregateProviderError(`No provider could complete the request.${formatFailures(failures)}`, failures);
|
|
865
1136
|
}
|
|
866
|
-
|
|
1137
|
+
function attachOperationUsageToError(error, snapshot) {
|
|
1138
|
+
if (error && typeof error === "object" && Object.isExtensible(error)) {
|
|
1139
|
+
try {
|
|
1140
|
+
Object.defineProperty(error, "operationUsage", {
|
|
1141
|
+
configurable: true,
|
|
1142
|
+
enumerable: false,
|
|
1143
|
+
value: snapshot,
|
|
1144
|
+
});
|
|
1145
|
+
return error;
|
|
1146
|
+
}
|
|
1147
|
+
catch { }
|
|
1148
|
+
}
|
|
1149
|
+
const message = error instanceof Error
|
|
1150
|
+
? error.message
|
|
1151
|
+
: typeof error === "string" && error.trim()
|
|
1152
|
+
? error
|
|
1153
|
+
: "Provider operation failed";
|
|
1154
|
+
const wrapped = new Error(message, { cause: error });
|
|
1155
|
+
Object.defineProperty(wrapped, "operationUsage", {
|
|
1156
|
+
configurable: true,
|
|
1157
|
+
enumerable: false,
|
|
1158
|
+
value: snapshot,
|
|
1159
|
+
});
|
|
1160
|
+
return wrapped;
|
|
1161
|
+
}
|
|
1162
|
+
export async function completeWithProvider(request, options = {}) {
|
|
1163
|
+
const ledger = resolveOperationLedger(options);
|
|
1164
|
+
try {
|
|
1165
|
+
const result = await completeWithProviderOperation(request, options, ledger);
|
|
1166
|
+
ledger.settle("completed");
|
|
1167
|
+
return { ...result, operationUsage: ledger.snapshot() };
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
ledger.settle(operationTerminalOutcome(error, request.signal));
|
|
1171
|
+
throw attachOperationUsageToError(error, ledger.snapshot());
|
|
1172
|
+
}
|
|
1173
|
+
finally {
|
|
1174
|
+
options.onOperationUsage?.(ledger.snapshot());
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async function streamWithProviderOperation(request, onToken, onStatusOrOptions, ledger) {
|
|
867
1178
|
const options = typeof onStatusOrOptions === "function"
|
|
868
1179
|
? { onStatus: onStatusOrOptions }
|
|
869
1180
|
: onStatusOrOptions ?? {};
|
|
1181
|
+
const operation = ledger ?? resolveOperationLedger(options);
|
|
1182
|
+
const relayToken = (token) => {
|
|
1183
|
+
operation.noteSemanticOutput();
|
|
1184
|
+
onToken(token);
|
|
1185
|
+
};
|
|
1186
|
+
const downstreamToolDelta = request.onToolCallDelta;
|
|
870
1187
|
const config = getConfig();
|
|
1188
|
+
const singleDispatch = options.singleDispatch === true;
|
|
871
1189
|
const requested = request.provider ?? config.defaultProvider;
|
|
872
1190
|
const providerImpl = getProvider(requested);
|
|
873
1191
|
const isDefaultModel = !request.model || request.model === providerImpl.defaultModel;
|
|
874
|
-
const fallbackEnabled =
|
|
1192
|
+
const fallbackEnabled = !singleDispatch &&
|
|
1193
|
+
config.providerFallback &&
|
|
875
1194
|
(await requestedRealKeyCount(requested)) !== 1 &&
|
|
876
1195
|
(isDefaultModel || request.allowModelFallback === true);
|
|
877
|
-
const order =
|
|
1196
|
+
const order = singleDispatch
|
|
1197
|
+
? [requested]
|
|
1198
|
+
: buildFallbackChain(requested, config.freeOnly, fallbackEnabled, request.preferModelFallback === true);
|
|
878
1199
|
const failures = [];
|
|
879
1200
|
const emitStatus = options.onStatus ?? ((message) => onToken(message));
|
|
880
1201
|
const emitKey = makeKeyEmitter(emitStatus, options.onKeyEvent);
|
|
@@ -889,7 +1210,28 @@ export async function streamWithProvider(request, onToken, onStatusOrOptions) {
|
|
|
889
1210
|
const model = providerId === requested
|
|
890
1211
|
? (request.model ?? provider.defaultModel)
|
|
891
1212
|
: provider.defaultModel;
|
|
892
|
-
const routeRequest =
|
|
1213
|
+
const routeRequest = {
|
|
1214
|
+
...requestForRoute(request, providerId, model),
|
|
1215
|
+
attemptUsage: operation,
|
|
1216
|
+
...(downstreamToolDelta
|
|
1217
|
+
? {
|
|
1218
|
+
onToolCallDelta: (delta) => {
|
|
1219
|
+
operation.noteSemanticOutput();
|
|
1220
|
+
downstreamToolDelta(delta);
|
|
1221
|
+
},
|
|
1222
|
+
}
|
|
1223
|
+
: {}),
|
|
1224
|
+
...(options.onStreamEvent
|
|
1225
|
+
? {
|
|
1226
|
+
onStreamEvent: (event) => {
|
|
1227
|
+
if (isSemanticStreamOutputEvent(event)) {
|
|
1228
|
+
operation.noteSemanticOutput();
|
|
1229
|
+
}
|
|
1230
|
+
options.onStreamEvent(event);
|
|
1231
|
+
},
|
|
1232
|
+
}
|
|
1233
|
+
: {}),
|
|
1234
|
+
};
|
|
893
1235
|
try {
|
|
894
1236
|
const result = await runWithKeyRotation({
|
|
895
1237
|
providerId,
|
|
@@ -898,18 +1240,30 @@ export async function streamWithProvider(request, onToken, onStatusOrOptions) {
|
|
|
898
1240
|
model,
|
|
899
1241
|
emitKey,
|
|
900
1242
|
mode: "stream",
|
|
901
|
-
|
|
1243
|
+
initialAttemptReason: providerId === requested ? "initial" : "fallback",
|
|
1244
|
+
onToken: relayToken,
|
|
902
1245
|
onStatus: emitStatus,
|
|
903
1246
|
...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
|
1247
|
+
...(singleDispatch ? { singleDispatch: true } : {}),
|
|
1248
|
+
...(options.onSuccessfulRequest
|
|
1249
|
+
? { onSuccessfulRequest: options.onSuccessfulRequest }
|
|
1250
|
+
: {}),
|
|
904
1251
|
});
|
|
905
1252
|
if (providerId !== requested) {
|
|
906
|
-
|
|
907
|
-
|
|
1253
|
+
if (options.adoptFallback === true) {
|
|
1254
|
+
setDefaultProvider(providerId);
|
|
1255
|
+
setProviderModel(providerId, result.model || model);
|
|
1256
|
+
}
|
|
908
1257
|
options.onStatus?.(`switching to ${providerId}/${result.model || model} after ${requested} failed`);
|
|
909
1258
|
}
|
|
910
1259
|
return result;
|
|
911
1260
|
}
|
|
912
1261
|
catch (error) {
|
|
1262
|
+
if (isOperationPolicyError(error)) {
|
|
1263
|
+
if (failures.length === 0)
|
|
1264
|
+
throw error;
|
|
1265
|
+
throw aggregateProviderError(`No provider could stream the request.${formatFailures(failures)}`, failures, streamEmittedBytes(error));
|
|
1266
|
+
}
|
|
913
1267
|
failures.push({
|
|
914
1268
|
provider: providerId,
|
|
915
1269
|
message: failureMessageFor(providerId, error),
|
|
@@ -927,6 +1281,24 @@ export async function streamWithProvider(request, onToken, onStatusOrOptions) {
|
|
|
927
1281
|
}
|
|
928
1282
|
throw aggregateProviderError(`No provider could stream the request.${formatFailures(failures)}`, failures);
|
|
929
1283
|
}
|
|
1284
|
+
export async function streamWithProvider(request, onToken, onStatusOrOptions) {
|
|
1285
|
+
const options = typeof onStatusOrOptions === "function"
|
|
1286
|
+
? { onStatus: onStatusOrOptions }
|
|
1287
|
+
: onStatusOrOptions ?? {};
|
|
1288
|
+
const ledger = resolveOperationLedger(options);
|
|
1289
|
+
try {
|
|
1290
|
+
const result = await streamWithProviderOperation(request, onToken, options, ledger);
|
|
1291
|
+
ledger.settle("completed");
|
|
1292
|
+
return { ...result, operationUsage: ledger.snapshot() };
|
|
1293
|
+
}
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
ledger.settle(operationTerminalOutcome(error, request.signal));
|
|
1296
|
+
throw attachOperationUsageToError(error, ledger.snapshot());
|
|
1297
|
+
}
|
|
1298
|
+
finally {
|
|
1299
|
+
options.onOperationUsage?.(ledger.snapshot());
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
930
1302
|
export async function pingProvider(providerId, secretOverride) {
|
|
931
1303
|
const provider = getProvider(providerId);
|
|
932
1304
|
const resolved = await providerAuth(providerId);
|