| import type { SessionEntry } from "../config/sessions.js"; |
| import { formatProviderModelRef } from "./model-runtime.js"; |
| import type { RuntimeFallbackAttempt } from "./reply/agent-runner-execution.js"; |
|
|
| const FALLBACK_REASON_PART_MAX = 80; |
|
|
| export type FallbackNoticeState = Pick< |
| SessionEntry, |
| "fallbackNoticeSelectedModel" | "fallbackNoticeActiveModel" | "fallbackNoticeReason" |
| >; |
|
|
| export function normalizeFallbackModelRef(value?: string): string | undefined { |
| const trimmed = String(value ?? "").trim(); |
| return trimmed || undefined; |
| } |
|
|
| function truncateFallbackReasonPart(value: string, max = FALLBACK_REASON_PART_MAX): string { |
| const text = String(value ?? "") |
| .replace(/\s+/g, " ") |
| .trim(); |
| if (text.length <= max) { |
| return text; |
| } |
| return `${text.slice(0, Math.max(0, max - 1)).trimEnd()}…`; |
| } |
|
|
| export function formatFallbackAttemptReason(attempt: RuntimeFallbackAttempt): string { |
| const reason = attempt.reason?.trim(); |
| if (reason) { |
| return reason.replace(/_/g, " "); |
| } |
| const code = attempt.code?.trim(); |
| if (code) { |
| return code; |
| } |
| if (typeof attempt.status === "number") { |
| return `HTTP ${attempt.status}`; |
| } |
| return truncateFallbackReasonPart(attempt.error || "error"); |
| } |
|
|
| function formatFallbackAttemptSummary(attempt: RuntimeFallbackAttempt): string { |
| return `${formatProviderModelRef(attempt.provider, attempt.model)} ${formatFallbackAttemptReason(attempt)}`; |
| } |
|
|
| export function buildFallbackReasonSummary(attempts: RuntimeFallbackAttempt[]): string { |
| const firstAttempt = attempts[0]; |
| const firstReason = firstAttempt |
| ? formatFallbackAttemptReason(firstAttempt) |
| : "selected model unavailable"; |
| const moreAttempts = attempts.length > 1 ? ` (+${attempts.length - 1} more attempts)` : ""; |
| return `${truncateFallbackReasonPart(firstReason)}${moreAttempts}`; |
| } |
|
|
| export function buildFallbackAttemptSummaries(attempts: RuntimeFallbackAttempt[]): string[] { |
| return attempts.map((attempt) => |
| truncateFallbackReasonPart(formatFallbackAttemptSummary(attempt)), |
| ); |
| } |
|
|
| export function buildFallbackNotice(params: { |
| selectedProvider: string; |
| selectedModel: string; |
| activeProvider: string; |
| activeModel: string; |
| attempts: RuntimeFallbackAttempt[]; |
| }): string | null { |
| const selected = formatProviderModelRef(params.selectedProvider, params.selectedModel); |
| const active = formatProviderModelRef(params.activeProvider, params.activeModel); |
| if (selected === active) { |
| return null; |
| } |
| const reasonSummary = buildFallbackReasonSummary(params.attempts); |
| return `↪️ Model Fallback: ${active} (selected ${selected}; ${reasonSummary})`; |
| } |
|
|
| export function buildFallbackClearedNotice(params: { |
| selectedProvider: string; |
| selectedModel: string; |
| previousActiveModel?: string; |
| }): string { |
| const selected = formatProviderModelRef(params.selectedProvider, params.selectedModel); |
| const previous = normalizeFallbackModelRef(params.previousActiveModel); |
| if (previous && previous !== selected) { |
| return `↪️ Model Fallback cleared: ${selected} (was ${previous})`; |
| } |
| return `↪️ Model Fallback cleared: ${selected}`; |
| } |
|
|
| export function resolveActiveFallbackState(params: { |
| selectedModelRef: string; |
| activeModelRef: string; |
| state?: FallbackNoticeState; |
| }): { active: boolean; reason?: string } { |
| const selected = normalizeFallbackModelRef(params.state?.fallbackNoticeSelectedModel); |
| const active = normalizeFallbackModelRef(params.state?.fallbackNoticeActiveModel); |
| const reason = normalizeFallbackModelRef(params.state?.fallbackNoticeReason); |
| const fallbackActive = |
| params.selectedModelRef !== params.activeModelRef && |
| selected === params.selectedModelRef && |
| active === params.activeModelRef; |
| return { |
| active: fallbackActive, |
| reason: fallbackActive ? reason : undefined, |
| }; |
| } |
|
|
| export type ResolvedFallbackTransition = { |
| selectedModelRef: string; |
| activeModelRef: string; |
| fallbackActive: boolean; |
| fallbackTransitioned: boolean; |
| fallbackCleared: boolean; |
| reasonSummary: string; |
| attemptSummaries: string[]; |
| previousState: { |
| selectedModel?: string; |
| activeModel?: string; |
| reason?: string; |
| }; |
| nextState: { |
| selectedModel?: string; |
| activeModel?: string; |
| reason?: string; |
| }; |
| stateChanged: boolean; |
| }; |
|
|
| export function resolveFallbackTransition(params: { |
| selectedProvider: string; |
| selectedModel: string; |
| activeProvider: string; |
| activeModel: string; |
| attempts: RuntimeFallbackAttempt[]; |
| state?: FallbackNoticeState; |
| }): ResolvedFallbackTransition { |
| const selectedModelRef = formatProviderModelRef(params.selectedProvider, params.selectedModel); |
| const activeModelRef = formatProviderModelRef(params.activeProvider, params.activeModel); |
| const previousState = { |
| selectedModel: normalizeFallbackModelRef(params.state?.fallbackNoticeSelectedModel), |
| activeModel: normalizeFallbackModelRef(params.state?.fallbackNoticeActiveModel), |
| reason: normalizeFallbackModelRef(params.state?.fallbackNoticeReason), |
| }; |
| const fallbackActive = selectedModelRef !== activeModelRef; |
| const fallbackTransitioned = |
| fallbackActive && |
| (previousState.selectedModel !== selectedModelRef || |
| previousState.activeModel !== activeModelRef); |
| const fallbackCleared = |
| !fallbackActive && Boolean(previousState.selectedModel || previousState.activeModel); |
| const reasonSummary = buildFallbackReasonSummary(params.attempts); |
| const attemptSummaries = buildFallbackAttemptSummaries(params.attempts); |
| const nextState = fallbackActive |
| ? { |
| selectedModel: selectedModelRef, |
| activeModel: activeModelRef, |
| reason: reasonSummary, |
| } |
| : { |
| selectedModel: undefined, |
| activeModel: undefined, |
| reason: undefined, |
| }; |
| const stateChanged = |
| previousState.selectedModel !== nextState.selectedModel || |
| previousState.activeModel !== nextState.activeModel || |
| previousState.reason !== nextState.reason; |
| return { |
| selectedModelRef, |
| activeModelRef, |
| fallbackActive, |
| fallbackTransitioned, |
| fallbackCleared, |
| reasonSummary, |
| attemptSummaries, |
| previousState, |
| nextState, |
| stateChanged, |
| }; |
| } |
|
|