From 5591f86b146247d493779835845b3e667f6bc189 Mon Sep 17 00:00:00 2001 From: Kaguya-19 Date: Wed, 24 Jun 2026 15:59:33 +0800 Subject: [PATCH 1/5] fix(router): honor media model capabilities --- src/router/RouterRuntime.ts | 141 +++++++++++++++++++++++++- src/router/protocol/decision.ts | 5 + src/router/utils/mediaRequirements.ts | 71 +++++++++++++ 3 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/router/utils/mediaRequirements.ts diff --git a/src/router/RouterRuntime.ts b/src/router/RouterRuntime.ts index a40dbbb9..187d9f4b 100644 --- a/src/router/RouterRuntime.ts +++ b/src/router/RouterRuntime.ts @@ -3,6 +3,7 @@ import type { CanonicalModelRequest, ModelRuntime, } from "../model/index.js"; +import type { InputModality } from "../model/index.js"; import { DEFAULT_SUBAGENT_MAX_TOKENS, DEFAULT_SUBAGENT_POLICY, @@ -37,6 +38,10 @@ import { import { TokenStatsCollector } from "./stats/TokenStatsCollector.js"; import { classifyAndRoute } from "./tokenSaver/classifyAndRoute.js"; import { countMessagesTokens, countResponseTokens, dispose as disposeTokenizer } from "./utils/countTokens.js"; +import { + collectRequiredInputModalities, + missingInputModalities, +} from "./utils/mediaRequirements.js"; import type { TelemetryClient } from "../telemetry/index.js"; export type RouterRuntimeDeps = { @@ -113,6 +118,90 @@ export function createRouterRuntime( return tracker; } + function missingForModel( + ref: RouterModelRef, + required: readonly InputModality[], + ): InputModality[] { + if (required.length === 0) { + return []; + } + try { + return missingInputModalities( + deps.modelRuntime.getMultimodal(ref.provider, ref.model), + required, + ); + } catch { + return [...required]; + } + } + + function supportsMediaRequirements( + ref: RouterModelRef, + required: readonly InputModality[], + ): boolean { + return missingForModel(ref, required).length === 0; + } + + function fallbackCandidatesFor(scenarioType: RouterScenarioType): RouterModelRef[] { + const candidates: RouterModelRef[] = []; + const add = (refs: RouterModelRef[] | undefined) => { + for (const ref of refs ?? []) { + const id = ref.id || `${ref.provider}/${ref.model}`; + if (!candidates.some((candidate) => candidate.provider === ref.provider && candidate.model === ref.model)) { + candidates.push({ ...ref, id }); + } + } + }; + add((config.fallback as Record | undefined)?.[scenarioType]); + add(config.fallback?.default); + return candidates; + } + + function findCompatibleFallback( + scenarioType: RouterScenarioType, + required: readonly InputModality[], + ): RouterModelRef | undefined { + return fallbackCandidatesFor(scenarioType) + .find((ref) => supportsMediaRequirements(ref, required)); + } + + function rerouteDecisionForMedia( + decision: RouterDecision, + messages: CanonicalModelRequest["messages"], + mutations: RouterMutationsLog, + ): RouterMutationsLog { + const required = collectRequiredInputModalities(messages); + if (required.length === 0) { + return mutations; + } + + const selected: RouterModelRef = { + id: `${decision.provider}/${decision.model}`, + provider: decision.provider, + model: decision.model, + }; + if (supportsMediaRequirements(selected, required)) { + return mutations; + } + + const replacement = findCompatibleFallback(decision.scenarioType, required); + if (!replacement) { + return mutations; + } + + decision.provider = replacement.provider; + decision.model = replacement.model; + decision.resolvedFrom = "fallback"; + return { + ...mutations, + mediaCapabilityRerouted: { + required: [...required], + from: selected.id, + to: replacement.id || `${replacement.provider}/${replacement.model}`, + }, + }; + } + async function resolveCustom( input: RouterDecisionInput, ): Promise | undefined> { @@ -329,6 +418,9 @@ export function createRouterRuntime( mutations = { ...mutations, subagentTagStripped: true }; } + const mediaMessages = decision.requestPatch?.messages ?? input.request.messages; + mutations = rerouteDecisionForMedia(decision, mediaMessages, mutations); + decision.mutations = mutations; sessionStore.set({ @@ -375,10 +467,21 @@ export function createRouterRuntime( ): AsyncIterable { const startedAt = (deps.now?.() ?? new Date()).toISOString(); const fallbackPlan = planFallback(config.fallback, decision.scenarioType); + const baseRequest = applyDecisionToRequest(decision, request); + const requiredModalities = collectRequiredInputModalities(baseRequest.messages); + const requestedAttempt: RouterModelRef = { + id: `${decision.provider}/${decision.model}`, + provider: decision.provider, + model: decision.model, + }; const attempts: RouterModelRef[] = [ - { id: `${decision.provider}/${decision.model}`, provider: decision.provider, model: decision.model }, + requestedAttempt, ...fallbackPlan.attempts, - ]; + ].filter((attempt, index, all) => + all.findIndex((candidate) => + candidate.provider === attempt.provider && candidate.model === attempt.model + ) === index + ).filter((attempt) => supportsMediaRequirements(attempt, requiredModalities)); const zeroUsageMax = Math.max(1, config.zeroUsageRetry?.maxAttempts ?? 5); const zeroUsageEnabled = config.zeroUsageRetry?.enabled ?? true; const transientRetryEnabled = config.transientRetry?.enabled ?? true; @@ -393,6 +496,22 @@ export function createRouterRuntime( let lastDecision: RouterDecision = decision; let lastHasYieldedContent = false; + if (attempts.length === 0) { + const missing = missingForModel(requestedAttempt, requiredModalities); + const error = createUnsupportedMediaError(requestedAttempt, requiredModalities, missing); + events.emit({ + type: "pilotdeck_router_execute_failed", + sessionId: ctx.sessionId, + turnId: ctx.turnId, + scenarioType: decision.scenarioType, + provider: requestedAttempt.provider, + model: requestedAttempt.model, + error, + }); + yield { type: "error", error }; + return; + } + outer: for (let attemptIndex = 0; attemptIndex < attempts.length; attemptIndex += 1) { if (ctx.abortSignal?.aborted) { return; @@ -944,6 +1063,24 @@ function classifyRetryReason(errorCode: string): "rate_limit" | "server_error" | return "server_error"; } +function createUnsupportedMediaError( + attempt: RouterModelRef, + required: readonly InputModality[], + missing: readonly InputModality[], +): import("../model/index.js").CanonicalModelError { + const missingText = (missing.length > 0 ? missing : required).join(", "); + const requiredText = required.join(", "); + return { + provider: attempt.provider, + protocol: "openai", + code: "unsupported_modality", + message: + `Router could not find a configured fallback model for ${attempt.provider}/${attempt.model} ` + + `that supports required input modalities: ${requiredText}. Missing: ${missingText}.`, + retryable: false, + }; +} + function extractPartialText(buffered: CanonicalModelEvent[]): string { let text = ""; for (const ev of buffered) { diff --git a/src/router/protocol/decision.ts b/src/router/protocol/decision.ts index 2c2e2d4c..90b38f6c 100644 --- a/src/router/protocol/decision.ts +++ b/src/router/protocol/decision.ts @@ -17,6 +17,11 @@ export type RouterMutationsLog = { asyncAgentLaunchedRewritten?: boolean; subagentTagStripped?: boolean; subagentModelOverride?: boolean; + mediaCapabilityRerouted?: { + required: import("../../model/protocol/multimodal.js").InputModality[]; + from: string; + to: string; + }; }; export type RouterRequestPatch = Pick< diff --git a/src/router/utils/mediaRequirements.ts b/src/router/utils/mediaRequirements.ts new file mode 100644 index 00000000..a3ccb816 --- /dev/null +++ b/src/router/utils/mediaRequirements.ts @@ -0,0 +1,71 @@ +import type { + CanonicalContentBlock, + CanonicalMessage, +} from "../../model/index.js"; +import type { InputModality, MultimodalConstraints } from "../../model/index.js"; + +const MEDIA_MODALITY_ORDER: InputModality[] = ["image", "pdf", "audio"]; + +export function collectRequiredInputModalities( + messages: CanonicalMessage[], +): InputModality[] { + const required = new Set(); + + for (const message of messages) { + for (const block of message.content) { + collectFromBlock(block, required); + } + } + + return MEDIA_MODALITY_ORDER.filter((modality) => required.has(modality)); +} + +export function missingInputModalities( + constraints: MultimodalConstraints, + required: readonly InputModality[], +): InputModality[] { + if (required.length === 0) { + return []; + } + const supported = new Set(constraints.input); + return required.filter((modality) => !supported.has(modality)); +} + +export function supportsRequiredModalities( + constraints: MultimodalConstraints, + required: readonly InputModality[], +): boolean { + return missingInputModalities(constraints, required).length === 0; +} + +function collectFromBlock( + block: CanonicalContentBlock, + required: Set, +): void { + switch (block.type) { + case "image": + required.add("image"); + return; + case "pdf": + required.add("pdf"); + return; + case "audio": + required.add("audio"); + return; + case "tool_result": + for (const item of block.content) { + if (item.type === "image" || item.type === "pdf") { + required.add(item.type); + } + } + return; + case "media_reference": + required.add(block.mediaType); + return; + case "text": + case "thinking": + case "tool_call": + case "tool_result_reference": + return; + } +} From e88b239fa6ef17a8d72bc9f630118a4594acdc96 Mon Sep 17 00:00:00 2001 From: Kaguya-19 Date: Wed, 24 Jun 2026 15:59:47 +0800 Subject: [PATCH 2/5] fix(model): reject unsupported media inputs --- src/model/request/validateModelRequest.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/model/request/validateModelRequest.ts b/src/model/request/validateModelRequest.ts index ca34e769..fb7100a6 100644 --- a/src/model/request/validateModelRequest.ts +++ b/src/model/request/validateModelRequest.ts @@ -1,6 +1,6 @@ import type { CanonicalModelRequest, ModelConfig, ModelDefinition, ProviderConfig } from "../protocol/canonical.js"; import { ModelRequestError } from "../protocol/errors.js"; -import { assertContentSupported, downgradeUnsupportedContent } from "../protocol/multimodal.js"; +import { assertContentSupported } from "../protocol/multimodal.js"; export type ResolvedModelRequest = { provider: ProviderConfig; @@ -39,8 +39,6 @@ export function validateModelRequest( throw new ModelRequestError("unsupported_tool_use", `Model ${request.model} does not support tools.`); } - downgradeUnsupportedContent(request.messages, model.multimodal); - for (const message of request.messages) { assertContentSupported(message.content, model.multimodal); } From eefc510a56175c215a56277a51941edd8ca30f6b Mon Sep 17 00:00:00 2001 From: Kaguya-19 Date: Wed, 24 Jun 2026 16:23:03 +0800 Subject: [PATCH 3/5] fix(anthropic): preserve thinking signatures --- src/model/providers/anthropic/request.ts | 12 ++++++++++-- src/model/providers/anthropic/response.ts | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/model/providers/anthropic/request.ts b/src/model/providers/anthropic/request.ts index d0276ac6..81828b66 100644 --- a/src/model/providers/anthropic/request.ts +++ b/src/model/providers/anthropic/request.ts @@ -140,8 +140,16 @@ function toAnthropicContentBlock(block: CanonicalContentBlock): unknown { switch (block.type) { case "text": return { type: "text", text: block.text }; - case "thinking": - return { type: "thinking", thinking: block.text }; + case "thinking": { + const thinking: { type: "thinking"; thinking: string; signature?: string } = { + type: "thinking", + thinking: block.text, + }; + if (block.signature) { + thinking.signature = block.signature; + } + return thinking; + } case "image": return block.source === "url" ? { type: "image", source: { type: "url", url: block.data } } diff --git a/src/model/providers/anthropic/response.ts b/src/model/providers/anthropic/response.ts index cea3e337..0ff37157 100644 --- a/src/model/providers/anthropic/response.ts +++ b/src/model/providers/anthropic/response.ts @@ -1,6 +1,7 @@ import type { CanonicalContentBlock, CanonicalModelResponse, + CanonicalThinkingBlock, CanonicalToolCallBlock, } from "../../protocol/canonical.js"; import { normalizeAnthropicFinishReason } from "../../response/normalizeFinishReason.js"; @@ -26,8 +27,17 @@ function toCanonicalContentBlock(block: unknown): CanonicalContentBlock[] { switch (record.type) { case "text": return [{ type: "text", text: readString(record.text) ?? "" }]; - case "thinking": - return [{ type: "thinking", text: readString(record.thinking) ?? readString(record.text) ?? "" }]; + case "thinking": { + const thinking: CanonicalThinkingBlock = { + type: "thinking", + text: readString(record.thinking) ?? readString(record.text) ?? "", + }; + const signature = readString(record.signature); + if (signature) { + thinking.signature = signature; + } + return [thinking]; + } case "tool_use": return [ { From c916a083b0152912475b88bbd0b5a97a019afe17 Mon Sep 17 00:00:00 2001 From: Kaguya-19 Date: Wed, 24 Jun 2026 17:12:41 +0800 Subject: [PATCH 4/5] fix(router): keep orchestration cache stable --- src/router/RouterRuntime.ts | 22 --- src/router/orchestrate/applyOrchestration.ts | 178 ++----------------- src/router/protocol/decision.ts | 1 + src/router/utils/mediaRequirements.ts | 3 - 4 files changed, 13 insertions(+), 191 deletions(-) diff --git a/src/router/RouterRuntime.ts b/src/router/RouterRuntime.ts index 187d9f4b..82b21c60 100644 --- a/src/router/RouterRuntime.ts +++ b/src/router/RouterRuntime.ts @@ -368,38 +368,16 @@ export function createRouterRuntime( `[router] decision: tier=${tokenSaverTier}, model=${selection.provider}/${selection.model}, orchGate=${orchGate}, alreadyOrch=${alreadyOrchestrating}, resolvedFrom=${resolvedFrom}`, ); - let skillPrompt: string | undefined; - if ( - config.autoOrchestrate?.enabled && - orchGate && - input.isMainAgent && - config.autoOrchestrate.skillExtensionId && - deps.loadSkillPrompt - ) { - try { - skillPrompt = await deps.loadSkillPrompt(config.autoOrchestrate.skillExtensionId); - } catch { - skillPrompt = undefined; - } - } - let mutations: RouterMutationsLog = {}; if (config.autoOrchestrate?.enabled && orchGate) { const orchestrated = applyOrchestration({ - request: input.request, config: config.autoOrchestrate, isMainAgent: input.isMainAgent, tier: tokenSaverTier, alreadyOrchestrating, - skillPrompt, }); if (orchestrated.applied) { mutations = { ...mutations, ...orchestrated.mutations }; - decision.requestPatch = { - messages: orchestrated.request.messages, - tools: orchestrated.request.tools, - systemPrompt: orchestrated.request.systemPrompt, - }; decision.orchestrating = true; if (config.autoOrchestrate.mainAgentModel) { decision.provider = config.autoOrchestrate.mainAgentModel.provider; diff --git a/src/router/orchestrate/applyOrchestration.ts b/src/router/orchestrate/applyOrchestration.ts index 881f71b5..197600ae 100644 --- a/src/router/orchestrate/applyOrchestration.ts +++ b/src/router/orchestrate/applyOrchestration.ts @@ -1,200 +1,46 @@ -import type { - CanonicalMessage, - CanonicalModelRequest, - CanonicalToolSchema, -} from "../../model/index.js"; -import { DEFAULT_ORCHESTRATION_PROMPT, type RouterAutoOrchestrateConfig } from "../config/schema.js"; +import type { RouterAutoOrchestrateConfig } from "../config/schema.js"; import type { RouterMutationsLog } from "../protocol/decision.js"; export type OrchestrationInput = { - request: CanonicalModelRequest; config: RouterAutoOrchestrateConfig; isMainAgent: boolean; tier?: string; /** When true the session was already orchestrating on a prior turn. */ alreadyOrchestrating?: boolean; - /** - * Optional skill prompt loaded by the caller (typically through extension). - * The router does not load files directly — it just receives prepared text. - */ - skillPrompt?: string; }; export type OrchestrationResult = { - request: CanonicalModelRequest; mutations: RouterMutationsLog; - /** True when orchestration actually mutated the request. */ + /** True when orchestration is active for this turn. */ applied: boolean; }; export function applyOrchestration(input: OrchestrationInput): OrchestrationResult { - const { config, request, skillPrompt } = input; + const { config } = input; console.log( `[autoOrch] input: tier=${input.tier}, isMain=${input.isMainAgent}, alreadyOrch=${input.alreadyOrchestrating}, triggerTiers=${config.triggerTiers}`, ); if (!config.enabled || !input.isMainAgent) { - return { request, mutations: {}, applied: false }; + return { mutations: {}, applied: false }; } if (!input.alreadyOrchestrating) { const triggerTiers = config.triggerTiers ?? []; if (triggerTiers.length > 0 && (!input.tier || !triggerTiers.includes(input.tier))) { console.log(`[autoOrch] tier "${input.tier}" not in triggerTiers, skipping`); - return { request, mutations: {}, applied: false }; + return { mutations: {}, applied: false }; } } - let messages = request.messages; - let mutations: RouterMutationsLog = {}; - let mutated = false; - - const effectivePrompt = skillPrompt ?? config.orchestrationPrompt ?? DEFAULT_ORCHESTRATION_PROMPT; - if (effectivePrompt && effectivePrompt.length > 0) { - messages = injectOrchestrationPrompt(messages, effectivePrompt); - mutations = { - ...mutations, - orchestrationPromptInjected: { tier: input.tier ?? "main", chars: effectivePrompt.length }, - }; - mutated = true; - } - - let tools = request.tools; - if (tools && config.allowedTools && config.allowedTools.length > 0) { - const before = tools.length; - const allowed = new Set(config.allowedTools.map(n => n.toLowerCase())); - const filtered = tools.filter((tool: CanonicalToolSchema) => allowed.has(tool.name.toLowerCase())); - if (filtered.length !== before) { - tools = filtered; - mutations = { - ...mutations, - toolsStripped: { before, after: filtered.length, mode: "allowlist", patterns: config.allowedTools }, - }; - mutated = true; - } - if (filtered.length === 0 && before > 0) { - console.warn(`[autoOrch] WARNING: allowedTools filter matched 0 of ${before} tools — falling back to unfiltered to preserve API tools param`); - tools = request.tools; - } - } else if (tools && config.blockedTools && config.blockedTools.length > 0) { - const before = tools.length; - const blocked = new Set(config.blockedTools.map(n => n.toLowerCase())); - const filtered = tools.filter((tool: CanonicalToolSchema) => !blocked.has(tool.name.toLowerCase())); - if (filtered.length !== before) { - tools = filtered; - mutations = { - ...mutations, - toolsStripped: { before, after: filtered.length, mode: "blocklist", patterns: config.blockedTools }, - }; - mutated = true; - } - } - - let systemPrompt = request.systemPrompt; - if (config.slimSystemPrompt && systemPrompt && systemPrompt.length > 0) { - const trimmed = trimSystemPrompt(systemPrompt); - if (trimmed.text !== systemPrompt) { - mutations = { - ...mutations, - systemPromptSlim: { - from: systemPrompt.length, - to: trimmed.text.length, - preservedKeywords: trimmed.preservedKeywords, - }, - }; - systemPrompt = trimmed.text; - mutated = true; - } - } - - if (!mutated) { - console.log("[autoOrch] no mutations applied, orchestration skipped"); - return { request, mutations: {}, applied: false }; - } - - console.log(`[autoOrch] orchestration applied: promptInjected=${"orchestrationPromptInjected" in mutations}, toolsStripped=${"toolsStripped" in mutations}, sysPromptSlim=${"systemPromptSlim" in mutations}`); - return { - request: { - ...request, - messages, - tools, - systemPrompt, + const mutations: RouterMutationsLog = { + orchestrationActivated: { + tier: input.tier ?? "main", + continued: input.alreadyOrchestrating === true, }, + }; + console.log(`[autoOrch] orchestration active: continued=${input.alreadyOrchestrating === true}`); + return { mutations, applied: true, }; } - -function injectOrchestrationPrompt( - messages: CanonicalMessage[], - prompt: string, -): CanonicalMessage[] { - const reminder: CanonicalMessage = { - role: "user", - content: [{ type: "text", text: `\n${prompt}\n` }], - }; - return [reminder, ...messages]; -} - -const SLIM_HEADER = "You are an orchestration agent. Use the agent tool to delegate all work to sub-agents."; -const MEMORY_KEYWORDS = [ - "memory_search", "memory_overview", "memory_get", - "memory_list", "memory_flush", "memory_dream", - "ClawXMemory", "cache_control", -]; - -const PRESERVED_TAGS: { open: string; close: string }[] = [ - { open: "" }, - { open: "" }, - { open: "" }, - { open: "" }, -]; - -function trimSystemPrompt(prompt: string): { text: string; preservedKeywords: string[] } { - const lines = prompt.split("\n"); - const preservedKeywords: string[] = []; - const preserved: string[] = []; - let activeTag: (typeof PRESERVED_TAGS)[number] | null = null; - let inMemoryBlock = false; - - for (const line of lines) { - const lower = line.toLowerCase(); - - if (!activeTag) { - const match = PRESERVED_TAGS.find(tag => lower.includes(tag.open)); - if (match) { - activeTag = match; - preserved.push(line); - if (lower.includes(match.close)) { - preservedKeywords.push(match.open.slice(1)); - activeTag = null; - } - continue; - } - } - - if (activeTag) { - preserved.push(line); - if (lower.includes(activeTag.close)) { - preservedKeywords.push(activeTag.open.slice(1)); - activeTag = null; - } - continue; - } - - const isMemory = MEMORY_KEYWORDS.some(kw => lower.includes(kw.toLowerCase())); - if (isMemory) { - inMemoryBlock = true; - preserved.push(line); - preservedKeywords.push(line.trim().slice(0, 40)); - } else if (inMemoryBlock && line.trim().length > 0) { - preserved.push(line); - } else { - inMemoryBlock = false; - } - } - - const text = preserved.length > 0 - ? SLIM_HEADER + "\n\n" + preserved.join("\n") - : SLIM_HEADER; - return { text, preservedKeywords }; -} diff --git a/src/router/protocol/decision.ts b/src/router/protocol/decision.ts index 90b38f6c..d6bb422a 100644 --- a/src/router/protocol/decision.ts +++ b/src/router/protocol/decision.ts @@ -14,6 +14,7 @@ export type RouterMutationsLog = { systemPromptSlim?: { from: number; to: number; preservedKeywords: string[] }; toolsStripped?: { before: number; after: number; mode?: "allowlist" | "blocklist"; patterns: string[] }; orchestrationPromptInjected?: { tier: string; chars: number }; + orchestrationActivated?: { tier: string; continued: boolean }; asyncAgentLaunchedRewritten?: boolean; subagentTagStripped?: boolean; subagentModelOverride?: boolean; diff --git a/src/router/utils/mediaRequirements.ts b/src/router/utils/mediaRequirements.ts index a3ccb816..64c80780 100644 --- a/src/router/utils/mediaRequirements.ts +++ b/src/router/utils/mediaRequirements.ts @@ -59,9 +59,6 @@ function collectFromBlock( } } return; - case "media_reference": - required.add(block.mediaType); - return; case "text": case "thinking": case "tool_call": From 2f7e5970003f80d4ba523c4fc0e7e854beb8f1cc Mon Sep 17 00:00:00 2001 From: Kaguya-19 Date: Wed, 24 Jun 2026 18:38:43 +0800 Subject: [PATCH 5/5] fix(router): preserve model request errors --- src/router/RouterRuntime.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/router/RouterRuntime.ts b/src/router/RouterRuntime.ts index 82b21c60..7401ee61 100644 --- a/src/router/RouterRuntime.ts +++ b/src/router/RouterRuntime.ts @@ -3,6 +3,7 @@ import type { CanonicalModelRequest, ModelRuntime, } from "../model/index.js"; +import { ModelRequestError } from "../model/index.js"; import type { InputModality } from "../model/index.js"; import { DEFAULT_SUBAGENT_MAX_TOKENS, @@ -953,7 +954,7 @@ async function* streamAttempt( throw error; } const fromError = (error as { error?: import("../model/index.js").CanonicalModelError })?.error; - providerError = fromError ?? { + providerError = fromError ?? canonicalizeModelRequestError(error, request) ?? { provider: request.provider, protocol: "anthropic", code: classifyNetworkErrorCode(error), @@ -973,6 +974,24 @@ async function* streamAttempt( }; } +function canonicalizeModelRequestError( + error: unknown, + request: CanonicalModelRequest, +): import("../model/index.js").CanonicalModelError | undefined { + if (!(error instanceof ModelRequestError)) { + return undefined; + } + + return { + provider: request.provider, + protocol: "anthropic", + code: error.code, + message: error.message, + retryable: false, + raw: error.details, + }; +} + function abortableDelay(ms: number, signal?: AbortSignal): Promise { if (!signal) { return new Promise((resolve) => setTimeout(resolve, ms));