openclaw/src/agents/model-fallback-observation.ts

94 lines
3.4 KiB
TypeScript

import { createSubsystemLogger } from "../logging/subsystem.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import type { FallbackAttempt, ModelCandidate } from "./model-fallback.types.js";
import { buildTextObservationFields } from "./pi-embedded-error-observation.js";
import type { FailoverReason } from "./pi-embedded-helpers.js";
const decisionLog = createSubsystemLogger("model-fallback").child("decision");
function buildErrorObservationFields(error?: string): {
errorPreview?: string;
errorHash?: string;
errorFingerprint?: string;
httpCode?: string;
providerErrorType?: string;
providerErrorMessagePreview?: string;
requestIdHash?: string;
} {
const observed = buildTextObservationFields(error);
return {
errorPreview: observed.textPreview,
errorHash: observed.textHash,
errorFingerprint: observed.textFingerprint,
httpCode: observed.httpCode,
providerErrorType: observed.providerErrorType,
providerErrorMessagePreview: observed.providerErrorMessagePreview,
requestIdHash: observed.requestIdHash,
};
}
export function logModelFallbackDecision(params: {
decision:
| "skip_candidate"
| "probe_cooldown_candidate"
| "candidate_failed"
| "candidate_succeeded";
runId?: string;
requestedProvider: string;
requestedModel: string;
candidate: ModelCandidate;
attempt?: number;
total?: number;
reason?: FailoverReason | null;
status?: number;
code?: string;
error?: string;
nextCandidate?: ModelCandidate;
isPrimary?: boolean;
requestedModelMatched?: boolean;
fallbackConfigured?: boolean;
allowTransientCooldownProbe?: boolean;
profileCount?: number;
previousAttempts?: FallbackAttempt[];
}): void {
const nextText = params.nextCandidate
? `${sanitizeForLog(params.nextCandidate.provider)}/${sanitizeForLog(params.nextCandidate.model)}`
: "none";
const reasonText = params.reason ?? "unknown";
const observedError = buildErrorObservationFields(params.error);
decisionLog.warn("model fallback decision", {
event: "model_fallback_decision",
tags: ["error_handling", "model_fallback", params.decision],
runId: params.runId,
decision: params.decision,
requestedProvider: params.requestedProvider,
requestedModel: params.requestedModel,
candidateProvider: params.candidate.provider,
candidateModel: params.candidate.model,
attempt: params.attempt,
total: params.total,
reason: params.reason,
status: params.status,
code: params.code,
...observedError,
nextCandidateProvider: params.nextCandidate?.provider,
nextCandidateModel: params.nextCandidate?.model,
isPrimary: params.isPrimary,
requestedModelMatched: params.requestedModelMatched,
fallbackConfigured: params.fallbackConfigured,
allowTransientCooldownProbe: params.allowTransientCooldownProbe,
profileCount: params.profileCount,
previousAttempts: params.previousAttempts?.map((attempt) => ({
provider: attempt.provider,
model: attempt.model,
reason: attempt.reason,
status: attempt.status,
code: attempt.code,
...buildErrorObservationFields(attempt.error),
})),
consoleMessage:
`model fallback decision: decision=${params.decision} requested=${sanitizeForLog(params.requestedProvider)}/${sanitizeForLog(params.requestedModel)} ` +
`candidate=${sanitizeForLog(params.candidate.provider)}/${sanitizeForLog(params.candidate.model)} reason=${reasonText} next=${nextText}`,
});
}