181 lines
6.0 KiB
TypeScript
181 lines
6.0 KiB
TypeScript
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,
|
|
};
|
|
}
|