Fix cooldown fallback switching across model/runtime fallback hooks
This commit is contained in:
committed by
YeonGyu-Kim
parent
eb43bd7c43
commit
512bc05404
+108
-28
@@ -13,11 +13,15 @@ import {
|
||||
import {
|
||||
clearPendingModelFallback,
|
||||
clearSessionFallbackChain,
|
||||
setSessionFallbackChain,
|
||||
setPendingModelFallback,
|
||||
} from "../hooks/model-fallback/hook";
|
||||
import { getFallbackModelsForSession } from "../hooks/runtime-fallback/fallback-models";
|
||||
import { resetMessageCursor } from "../shared";
|
||||
import { getAgentConfigKey } from "../shared/agent-display-names";
|
||||
import { log } from "../shared/logger";
|
||||
import { shouldRetryError } from "../shared/model-error-classifier";
|
||||
import type { FallbackEntry } from "../shared/model-requirements";
|
||||
import { clearSessionModel, setSessionModel } from "../shared/session-model-state";
|
||||
import { deleteSessionTools } from "../shared/session-tools-store";
|
||||
import { lspManager } from "../tools";
|
||||
@@ -43,6 +47,28 @@ function normalizeFallbackModelID(modelID: string): string {
|
||||
.replace(/-high$/i, "");
|
||||
}
|
||||
|
||||
function normalizeRetryStatusMessage(message: string): string {
|
||||
return message
|
||||
.replace(/\[retrying in [^\]]*attempt\s*#\d+\]/gi, "[retrying]")
|
||||
.replace(/retrying in\s+[^(]*attempt\s*#\d+/gi, "retrying")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function extractRetryAttempt(statusAttempt: unknown, message: string): string {
|
||||
if (typeof statusAttempt === "number" && Number.isFinite(statusAttempt)) {
|
||||
return String(statusAttempt);
|
||||
}
|
||||
|
||||
const attemptMatch = message.match(/attempt\s*#\s*(\d+)/i);
|
||||
if (attemptMatch?.[1]) {
|
||||
return attemptMatch[1];
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
|
||||
function extractErrorName(error: unknown): string | undefined {
|
||||
if (isRecord(error) && typeof error.name === "string") return error.name;
|
||||
if (error instanceof Error) return error.name;
|
||||
@@ -97,6 +123,48 @@ function extractProviderModelFromErrorMessage(message: string): { providerID?: s
|
||||
|
||||
return {};
|
||||
}
|
||||
function parseFallbackModelEntry(
|
||||
model: string,
|
||||
defaultProviderID: string,
|
||||
): FallbackEntry | undefined {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) return undefined;
|
||||
|
||||
const parts = trimmed.split("/");
|
||||
const providerID = parts.length >= 2 ? parts[0].trim() : defaultProviderID;
|
||||
const rawModelID = parts.length >= 2 ? parts.slice(1).join("/").trim() : trimmed;
|
||||
if (!providerID || !rawModelID) return undefined;
|
||||
|
||||
const variantMatch = rawModelID.match(/^(.*)\(([^()]+)\)\s*$/);
|
||||
if (variantMatch) {
|
||||
const parsedModelID = variantMatch[1]?.trim();
|
||||
const parsedVariant = variantMatch[2]?.trim();
|
||||
if (parsedModelID && parsedVariant) {
|
||||
return { providers: [providerID], model: parsedModelID, variant: parsedVariant };
|
||||
}
|
||||
}
|
||||
|
||||
return { providers: [providerID], model: rawModelID };
|
||||
}
|
||||
|
||||
function applyUserConfiguredFallbackChain(
|
||||
sessionID: string,
|
||||
agentName: string,
|
||||
currentProviderID: string,
|
||||
pluginConfig: OhMyOpenCodeConfig,
|
||||
): void {
|
||||
const agentKey = getAgentConfigKey(agentName);
|
||||
const configuredFallbackModels = getFallbackModelsForSession(sessionID, agentKey, pluginConfig);
|
||||
if (configuredFallbackModels.length === 0) return;
|
||||
|
||||
const fallbackChain = configuredFallbackModels
|
||||
.map((model) => parseFallbackModelEntry(model, currentProviderID))
|
||||
.filter((entry): entry is FallbackEntry => entry !== undefined);
|
||||
|
||||
if (fallbackChain.length > 0) {
|
||||
setSessionFallbackChain(sessionID, fallbackChain);
|
||||
}
|
||||
}
|
||||
|
||||
function isCompactionAgent(agent: string): boolean {
|
||||
return agent.toLowerCase() === "compaction";
|
||||
@@ -116,6 +184,11 @@ export function createEventHandler(args: {
|
||||
client: {
|
||||
session: {
|
||||
abort: (input: { path: { id: string } }) => Promise<unknown>;
|
||||
promptAsync?: (input: {
|
||||
path: { id: string };
|
||||
body: { parts: Array<{ type: "text"; text: string }> };
|
||||
query: { directory: string };
|
||||
}) => Promise<unknown>;
|
||||
prompt: (input: {
|
||||
path: { id: string };
|
||||
body: { parts: Array<{ type: "text"; text: string }> };
|
||||
@@ -176,6 +249,29 @@ export function createEventHandler(args: {
|
||||
return !subagentSessions.has(sessionID);
|
||||
};
|
||||
|
||||
const autoContinueAfterFallback = async (sessionID: string, source: string): Promise<void> => {
|
||||
await pluginContext.client.session.abort({ path: { id: sessionID } }).catch((error) => {
|
||||
log("[event] model-fallback abort failed", { sessionID, source, error });
|
||||
});
|
||||
|
||||
const promptBody = {
|
||||
path: { id: sessionID },
|
||||
body: { parts: [{ type: "text" as const, text: "continue" }] },
|
||||
query: { directory: pluginContext.directory },
|
||||
};
|
||||
|
||||
if (typeof pluginContext.client.session.promptAsync === "function") {
|
||||
await pluginContext.client.session.promptAsync(promptBody).catch((error) => {
|
||||
log("[event] model-fallback promptAsync failed", { sessionID, source, error });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginContext.client.session.prompt(promptBody).catch((error) => {
|
||||
log("[event] model-fallback prompt failed", { sessionID, source, error });
|
||||
});
|
||||
};
|
||||
|
||||
return async (input): Promise<void> => {
|
||||
pruneRecentSyntheticIdles({
|
||||
recentSyntheticIdles,
|
||||
@@ -310,6 +406,7 @@ export function createEventHandler(args: {
|
||||
const currentProvider = (info?.providerID as string | undefined) ?? "opencode";
|
||||
const rawModel = (info?.modelID as string | undefined) ?? "claude-opus-4-6";
|
||||
const currentModel = normalizeFallbackModelID(rawModel);
|
||||
applyUserConfiguredFallbackChain(sessionID, agentName, currentProvider, args.pluginConfig);
|
||||
|
||||
const setFallback = setPendingModelFallback(sessionID, agentName, currentProvider, currentModel);
|
||||
|
||||
@@ -319,15 +416,7 @@ export function createEventHandler(args: {
|
||||
!hooks.stopContinuationGuard?.isStopped(sessionID)
|
||||
) {
|
||||
lastHandledModelErrorMessageID.set(sessionID, assistantMessageID);
|
||||
|
||||
await pluginContext.client.session.abort({ path: { id: sessionID } }).catch(() => {});
|
||||
await pluginContext.client.session
|
||||
.prompt({
|
||||
path: { id: sessionID },
|
||||
body: { parts: [{ type: "text", text: "continue" }] },
|
||||
query: { directory: pluginContext.directory },
|
||||
})
|
||||
.catch(() => {});
|
||||
await autoContinueAfterFallback(sessionID, "message.updated");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,10 +431,14 @@ export function createEventHandler(args: {
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const status = props?.status as { type?: string; attempt?: number; message?: string; next?: number } | undefined;
|
||||
|
||||
if (sessionID && status?.type === "retry" && isModelFallbackEnabled) {
|
||||
if (sessionID && status?.type === "retry" && isModelFallbackEnabled && !isRuntimeFallbackEnabled) {
|
||||
try {
|
||||
const retryMessage = typeof status.message === "string" ? status.message : "";
|
||||
const retryKey = `${status.attempt ?? "?"}:${status.next ?? "?"}:${retryMessage}`;
|
||||
const parsedForKey = extractProviderModelFromErrorMessage(retryMessage);
|
||||
const retryAttempt = extractRetryAttempt(status.attempt, retryMessage);
|
||||
// Deduplicate countdown updates for the same retry attempt/model.
|
||||
// Messages like "retrying in 7m 56s" change every second but should only trigger once.
|
||||
const retryKey = `${retryAttempt}:${parsedForKey.providerID ?? ""}/${parsedForKey.modelID ?? ""}:${normalizeRetryStatusMessage(retryMessage)}`;
|
||||
if (lastHandledRetryStatusKey.get(sessionID) === retryKey) {
|
||||
return;
|
||||
}
|
||||
@@ -370,6 +463,7 @@ export function createEventHandler(args: {
|
||||
const currentProvider = parsed.providerID ?? lastKnown?.providerID ?? "opencode";
|
||||
let currentModel = parsed.modelID ?? lastKnown?.modelID ?? "claude-opus-4-6";
|
||||
currentModel = normalizeFallbackModelID(currentModel);
|
||||
applyUserConfiguredFallbackChain(sessionID, agentName, currentProvider, args.pluginConfig);
|
||||
|
||||
const setFallback = setPendingModelFallback(sessionID, agentName, currentProvider, currentModel);
|
||||
|
||||
@@ -378,14 +472,7 @@ export function createEventHandler(args: {
|
||||
shouldAutoRetrySession(sessionID) &&
|
||||
!hooks.stopContinuationGuard?.isStopped(sessionID)
|
||||
) {
|
||||
await pluginContext.client.session.abort({ path: { id: sessionID } }).catch(() => {});
|
||||
await pluginContext.client.session
|
||||
.prompt({
|
||||
path: { id: sessionID },
|
||||
body: { parts: [{ type: "text", text: "continue" }] },
|
||||
query: { directory: pluginContext.directory },
|
||||
})
|
||||
.catch(() => {});
|
||||
await autoContinueAfterFallback(sessionID, "session.status");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,6 +535,7 @@ export function createEventHandler(args: {
|
||||
const currentProvider = (props?.providerID as string) || parsed.providerID || "opencode";
|
||||
let currentModel = (props?.modelID as string) || parsed.modelID || "claude-opus-4-6";
|
||||
currentModel = normalizeFallbackModelID(currentModel);
|
||||
applyUserConfiguredFallbackChain(sessionID, agentName, currentProvider, args.pluginConfig);
|
||||
|
||||
const setFallback = setPendingModelFallback(sessionID, agentName, currentProvider, currentModel);
|
||||
|
||||
@@ -456,15 +544,7 @@ export function createEventHandler(args: {
|
||||
shouldAutoRetrySession(sessionID) &&
|
||||
!hooks.stopContinuationGuard?.isStopped(sessionID)
|
||||
) {
|
||||
await pluginContext.client.session.abort({ path: { id: sessionID } }).catch(() => {});
|
||||
|
||||
await pluginContext.client.session
|
||||
.prompt({
|
||||
path: { id: sessionID },
|
||||
body: { parts: [{ type: "text", text: "continue" }] },
|
||||
query: { directory: pluginContext.directory },
|
||||
})
|
||||
.catch(() => {});
|
||||
await autoContinueAfterFallback(sessionID, "session.error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user