feat(gsd): implement auto-mode fallback model rotation on network errors (#386)
This commit is contained in:
parent
9794c6aed3
commit
a883008be6
3 changed files with 131 additions and 0 deletions
|
|
@ -39,6 +39,8 @@ import {
|
|||
loadEffectiveGSDPreferences,
|
||||
renderPreferencesForSystemPrompt,
|
||||
resolveAllSkillReferences,
|
||||
resolveModelWithFallbacksForUnit,
|
||||
getNextFallbackModel,
|
||||
} from "./preferences.js";
|
||||
import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "./skill-discovery.js";
|
||||
import {
|
||||
|
|
@ -339,6 +341,50 @@ export default function (pi: ExtensionAPI) {
|
|||
"errorMessage" in lastMsg && lastMsg.errorMessage
|
||||
? `: ${lastMsg.errorMessage}`
|
||||
: "";
|
||||
|
||||
const dash = getAutoDashboardData();
|
||||
if (dash.currentUnit) {
|
||||
const modelConfig = resolveModelWithFallbacksForUnit(dash.currentUnit.type);
|
||||
if (modelConfig && modelConfig.fallbacks.length > 0) {
|
||||
const availableModels = ctx.modelRegistry.getAvailable();
|
||||
const currentModelId = ctx.model?.id;
|
||||
|
||||
const nextModelId = getNextFallbackModel(currentModelId, modelConfig);
|
||||
|
||||
if (nextModelId) {
|
||||
let modelToSet;
|
||||
const slashIdx = nextModelId.indexOf("/");
|
||||
if (slashIdx !== -1) {
|
||||
const provider = nextModelId.substring(0, slashIdx);
|
||||
const id = nextModelId.substring(slashIdx + 1);
|
||||
modelToSet = availableModels.find(
|
||||
m => m.provider.toLowerCase() === provider.toLowerCase()
|
||||
&& m.id.toLowerCase() === id.toLowerCase()
|
||||
);
|
||||
} else {
|
||||
const currentProvider = ctx.model?.provider;
|
||||
const exactProviderMatch = availableModels.find(
|
||||
m => m.id === nextModelId && m.provider === currentProvider
|
||||
);
|
||||
modelToSet = exactProviderMatch ?? availableModels.find(m => m.id === nextModelId);
|
||||
}
|
||||
|
||||
if (modelToSet) {
|
||||
const ok = await pi.setModel(modelToSet, { persist: false });
|
||||
if (ok) {
|
||||
ctx.ui.notify(`Model error${errorDetail}. Switched to fallback: ${nextModelId} and resuming.`, "warning");
|
||||
// Trigger a generic "Continue execution" to resume the task since the previous attempt failed
|
||||
pi.sendMessage(
|
||||
{ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false },
|
||||
{ triggerTurn: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(ctx as any).log(`Auto-mode paused due to provider error${errorDetail}`);
|
||||
await pauseAuto(ctx, pi);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -515,6 +515,37 @@ export function resolveModelForUnit(unitType: string): string | undefined {
|
|||
* - Legacy: `planning: claude-opus-4-6`
|
||||
* - Extended: `planning: { model: claude-opus-4-6, fallbacks: [glm-5, minimax-m2.5] }`
|
||||
*/
|
||||
/**
|
||||
* Determines the next fallback model to try when the current model fails.
|
||||
* If the current model is not in the configured list, returns the primary model.
|
||||
* If the current model is the last in the list, returns undefined (exhausted).
|
||||
*/
|
||||
export function getNextFallbackModel(
|
||||
currentModelId: string | undefined,
|
||||
modelConfig: ResolvedModelConfig,
|
||||
): string | undefined {
|
||||
const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
|
||||
|
||||
if (!currentModelId) {
|
||||
return modelsToTry[0];
|
||||
}
|
||||
|
||||
let foundCurrent = false;
|
||||
for (let i = 0; i < modelsToTry.length; i++) {
|
||||
const mId = modelsToTry[i];
|
||||
// Check for exact match or provider/model suffix match
|
||||
if (mId === currentModelId || (mId.includes("/") && mId.endsWith(`/${currentModelId}`))) {
|
||||
foundCurrent = true;
|
||||
return modelsToTry[i + 1]; // Return the next one, or undefined if at the end
|
||||
}
|
||||
}
|
||||
|
||||
// If the current model wasn't in our preference list, default to starting the sequence
|
||||
if (!foundCurrent) {
|
||||
return modelsToTry[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedModelConfig | undefined {
|
||||
const prefs = loadEffectiveGSDPreferences();
|
||||
if (!prefs?.preferences.models) return undefined;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Instead of trying to mock out the entire `index.ts` extension initialization which touches
|
||||
// the disk and parses files, we test the logic via the standard test methods, or we can
|
||||
// just test that `resolveModelWithFallbacksForUnit` returns the correct format since
|
||||
// the fallback rotation logic itself was verified manually.
|
||||
|
||||
import { getNextFallbackModel } from "../preferences.ts";
|
||||
|
||||
test("getNextFallbackModel selects next fallback if current is a fallback", () => {
|
||||
const modelConfig = { primary: "model-a", fallbacks: ["model-b", "model-c"] };
|
||||
const currentModelId = "model-b";
|
||||
|
||||
const nextModelId = getNextFallbackModel(currentModelId, modelConfig);
|
||||
|
||||
assert.equal(nextModelId, "model-c", "should select next model after current fallback");
|
||||
});
|
||||
|
||||
test("getNextFallbackModel returns undefined if fallbacks exhausted", () => {
|
||||
const modelConfig = { primary: "model-a", fallbacks: ["model-b", "model-c"] };
|
||||
const currentModelId = "model-c";
|
||||
|
||||
const nextModelId = getNextFallbackModel(currentModelId, modelConfig);
|
||||
|
||||
assert.equal(nextModelId, undefined, "should return undefined when exhausted");
|
||||
});
|
||||
|
||||
test("getNextFallbackModel finds current model when formatted with provider", () => {
|
||||
const modelConfig = { primary: "p/model-a", fallbacks: ["p/model-b"] };
|
||||
const currentModelId = "model-a"; // context model doesn't always have provider in ID
|
||||
|
||||
const nextModelId = getNextFallbackModel(currentModelId, modelConfig);
|
||||
|
||||
assert.equal(nextModelId, "p/model-b", "should select next model after current with provider format");
|
||||
});
|
||||
|
||||
test("getNextFallbackModel returns primary if current model is not in the list", () => {
|
||||
const modelConfig = { primary: "model-a", fallbacks: ["model-b", "model-c"] };
|
||||
const currentModelId = "model-x"; // completely different model manually selected
|
||||
|
||||
const nextModelId = getNextFallbackModel(currentModelId, modelConfig);
|
||||
|
||||
assert.equal(nextModelId, "model-a", "should default to primary if current is unknown");
|
||||
});
|
||||
|
||||
test("getNextFallbackModel returns primary if current model is undefined", () => {
|
||||
const modelConfig = { primary: "model-a", fallbacks: ["model-b", "model-c"] };
|
||||
const currentModelId = undefined;
|
||||
|
||||
const nextModelId = getNextFallbackModel(currentModelId, modelConfig);
|
||||
|
||||
assert.equal(nextModelId, "model-a", "should default to primary if current is undefined");
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue