fix(model-resolver): respect UI model selection in agent initialization (#1158)
- Add uiSelectedModel parameter to resolveModelWithFallback() - Update model resolution priority: UI Selection → Config Override → Fallback → System Default - Pass config.model as uiSelectedModel in createBuiltinAgents() - Fix ProviderModelNotFoundError when model is unset in config but selected in UI
This commit is contained in:
parent
c7455708f8
commit
0c3fbd724b
@ -149,7 +149,8 @@ export async function createBuiltinAgents(
|
|||||||
gitMasterConfig?: GitMasterConfig,
|
gitMasterConfig?: GitMasterConfig,
|
||||||
discoveredSkills: LoadedSkill[] = [],
|
discoveredSkills: LoadedSkill[] = [],
|
||||||
client?: any,
|
client?: any,
|
||||||
browserProvider?: BrowserAutomationProvider
|
browserProvider?: BrowserAutomationProvider,
|
||||||
|
uiSelectedModel?: string
|
||||||
): Promise<Record<string, AgentConfig>> {
|
): Promise<Record<string, AgentConfig>> {
|
||||||
const connectedProviders = readConnectedProvidersCache()
|
const connectedProviders = readConnectedProvidersCache()
|
||||||
const availableModels = client
|
const availableModels = client
|
||||||
@ -198,6 +199,7 @@ export async function createBuiltinAgents(
|
|||||||
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
||||||
|
|
||||||
const resolution = resolveModelWithFallback({
|
const resolution = resolveModelWithFallback({
|
||||||
|
uiSelectedModel,
|
||||||
userModel: override?.model,
|
userModel: override?.model,
|
||||||
fallbackChain: requirement?.fallbackChain,
|
fallbackChain: requirement?.fallbackChain,
|
||||||
availableModels,
|
availableModels,
|
||||||
@ -241,6 +243,7 @@ export async function createBuiltinAgents(
|
|||||||
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||||
|
|
||||||
const sisyphusResolution = resolveModelWithFallback({
|
const sisyphusResolution = resolveModelWithFallback({
|
||||||
|
uiSelectedModel,
|
||||||
userModel: sisyphusOverride?.model,
|
userModel: sisyphusOverride?.model,
|
||||||
fallbackChain: sisyphusRequirement?.fallbackChain,
|
fallbackChain: sisyphusRequirement?.fallbackChain,
|
||||||
availableModels,
|
availableModels,
|
||||||
@ -282,6 +285,7 @@ export async function createBuiltinAgents(
|
|||||||
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
|
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||||
|
|
||||||
const atlasResolution = resolveModelWithFallback({
|
const atlasResolution = resolveModelWithFallback({
|
||||||
|
uiSelectedModel,
|
||||||
userModel: orchestratorOverride?.model,
|
userModel: orchestratorOverride?.model,
|
||||||
fallbackChain: atlasRequirement?.fallbackChain,
|
fallbackChain: atlasRequirement?.fallbackChain,
|
||||||
availableModels,
|
availableModels,
|
||||||
|
|||||||
@ -133,16 +133,20 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const browserProvider = pluginConfig.browser_automation_engine?.provider ?? "playwright";
|
const browserProvider = pluginConfig.browser_automation_engine?.provider ?? "playwright";
|
||||||
|
// config.model represents the currently active model in OpenCode (including UI selection)
|
||||||
|
// Pass it as uiSelectedModel so it takes highest priority in model resolution
|
||||||
|
const currentModel = config.model as string | undefined;
|
||||||
const builtinAgents = await createBuiltinAgents(
|
const builtinAgents = await createBuiltinAgents(
|
||||||
migratedDisabledAgents,
|
migratedDisabledAgents,
|
||||||
pluginConfig.agents,
|
pluginConfig.agents,
|
||||||
ctx.directory,
|
ctx.directory,
|
||||||
config.model as string | undefined,
|
undefined, // systemDefaultModel - let fallback chain handle this
|
||||||
pluginConfig.categories,
|
pluginConfig.categories,
|
||||||
pluginConfig.git_master,
|
pluginConfig.git_master,
|
||||||
allDiscoveredSkills,
|
allDiscoveredSkills,
|
||||||
ctx.client,
|
ctx.client,
|
||||||
browserProvider
|
browserProvider,
|
||||||
|
currentModel // uiSelectedModel - takes highest priority
|
||||||
);
|
);
|
||||||
|
|
||||||
// Claude Code agents: Do NOT apply permission migration
|
// Claude Code agents: Do NOT apply permission migration
|
||||||
@ -225,7 +229,6 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
pluginConfig.agents?.["prometheus"] as
|
pluginConfig.agents?.["prometheus"] as
|
||||||
| (Record<string, unknown> & { category?: string; model?: string; variant?: string })
|
| (Record<string, unknown> & { category?: string; model?: string; variant?: string })
|
||||||
| undefined;
|
| undefined;
|
||||||
const defaultModel = config.model as string | undefined;
|
|
||||||
|
|
||||||
const categoryConfig = prometheusOverride?.category
|
const categoryConfig = prometheusOverride?.category
|
||||||
? resolveCategoryConfig(
|
? resolveCategoryConfig(
|
||||||
@ -241,10 +244,11 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
: new Set<string>();
|
: new Set<string>();
|
||||||
|
|
||||||
const modelResolution = resolveModelWithFallback({
|
const modelResolution = resolveModelWithFallback({
|
||||||
|
uiSelectedModel: currentModel,
|
||||||
userModel: prometheusOverride?.model ?? categoryConfig?.model,
|
userModel: prometheusOverride?.model ?? categoryConfig?.model,
|
||||||
fallbackChain: prometheusRequirement?.fallbackChain,
|
fallbackChain: prometheusRequirement?.fallbackChain,
|
||||||
availableModels,
|
availableModels,
|
||||||
systemDefaultModel: defaultModel ?? "",
|
systemDefaultModel: undefined, // let fallback chain handle this
|
||||||
});
|
});
|
||||||
const resolvedModel = modelResolution?.model;
|
const resolvedModel = modelResolution?.model;
|
||||||
const resolvedVariant = modelResolution?.variant;
|
const resolvedVariant = modelResolution?.variant;
|
||||||
|
|||||||
@ -113,7 +113,80 @@ describe("resolveModelWithFallback", () => {
|
|||||||
logSpy.mockRestore()
|
logSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Step 1: Override", () => {
|
describe("Step 1: UI Selection (highest priority)", () => {
|
||||||
|
test("returns uiSelectedModel with override source when provided", () => {
|
||||||
|
// #given
|
||||||
|
const input: ExtendedModelResolutionInput = {
|
||||||
|
uiSelectedModel: "opencode/glm-4.7-free",
|
||||||
|
userModel: "anthropic/claude-opus-4-5",
|
||||||
|
fallbackChain: [
|
||||||
|
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-5" },
|
||||||
|
],
|
||||||
|
availableModels: new Set(["anthropic/claude-opus-4-5", "github-copilot/claude-opus-4-5-preview"]),
|
||||||
|
systemDefaultModel: "google/gemini-3-pro",
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = resolveModelWithFallback(input)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result!.model).toBe("opencode/glm-4.7-free")
|
||||||
|
expect(result!.source).toBe("override")
|
||||||
|
expect(logSpy).toHaveBeenCalledWith("Model resolved via UI selection", { model: "opencode/glm-4.7-free" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("UI selection takes priority over config override", () => {
|
||||||
|
// #given
|
||||||
|
const input: ExtendedModelResolutionInput = {
|
||||||
|
uiSelectedModel: "opencode/glm-4.7-free",
|
||||||
|
userModel: "anthropic/claude-opus-4-5",
|
||||||
|
availableModels: new Set(["anthropic/claude-opus-4-5"]),
|
||||||
|
systemDefaultModel: "google/gemini-3-pro",
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = resolveModelWithFallback(input)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result!.model).toBe("opencode/glm-4.7-free")
|
||||||
|
expect(result!.source).toBe("override")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("whitespace-only uiSelectedModel is treated as not provided", () => {
|
||||||
|
// #given
|
||||||
|
const input: ExtendedModelResolutionInput = {
|
||||||
|
uiSelectedModel: " ",
|
||||||
|
userModel: "anthropic/claude-opus-4-5",
|
||||||
|
availableModels: new Set(["anthropic/claude-opus-4-5"]),
|
||||||
|
systemDefaultModel: "google/gemini-3-pro",
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = resolveModelWithFallback(input)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||||
|
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-5" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("empty string uiSelectedModel falls through to config override", () => {
|
||||||
|
// #given
|
||||||
|
const input: ExtendedModelResolutionInput = {
|
||||||
|
uiSelectedModel: "",
|
||||||
|
userModel: "anthropic/claude-opus-4-5",
|
||||||
|
availableModels: new Set(["anthropic/claude-opus-4-5"]),
|
||||||
|
systemDefaultModel: "google/gemini-3-pro",
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = resolveModelWithFallback(input)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Step 2: Config Override", () => {
|
||||||
test("returns userModel with override source when userModel is provided", () => {
|
test("returns userModel with override source when userModel is provided", () => {
|
||||||
// #given
|
// #given
|
||||||
const input: ExtendedModelResolutionInput = {
|
const input: ExtendedModelResolutionInput = {
|
||||||
@ -131,7 +204,7 @@ describe("resolveModelWithFallback", () => {
|
|||||||
// #then
|
// #then
|
||||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||||
expect(result!.source).toBe("override")
|
expect(result!.source).toBe("override")
|
||||||
expect(logSpy).toHaveBeenCalledWith("Model resolved via override", { model: "anthropic/claude-opus-4-5" })
|
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-5" })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("override takes priority even if model not in availableModels", () => {
|
test("override takes priority even if model not in availableModels", () => {
|
||||||
@ -190,7 +263,7 @@ describe("resolveModelWithFallback", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Step 2: Provider fallback chain", () => {
|
describe("Step 3: Provider fallback chain", () => {
|
||||||
test("tries providers in order within entry and returns first match", () => {
|
test("tries providers in order within entry and returns first match", () => {
|
||||||
// #given
|
// #given
|
||||||
const input: ExtendedModelResolutionInput = {
|
const input: ExtendedModelResolutionInput = {
|
||||||
@ -317,7 +390,7 @@ describe("resolveModelWithFallback", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Step 3: System default fallback (no availability match)", () => {
|
describe("Step 4: System default fallback (no availability match)", () => {
|
||||||
test("returns system default when no availability match found in fallback chain", () => {
|
test("returns system default when no availability match found in fallback chain", () => {
|
||||||
// #given
|
// #given
|
||||||
const input: ExtendedModelResolutionInput = {
|
const input: ExtendedModelResolutionInput = {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export type ModelResolutionResult = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedModelResolutionInput = {
|
export type ExtendedModelResolutionInput = {
|
||||||
|
uiSelectedModel?: string
|
||||||
userModel?: string
|
userModel?: string
|
||||||
fallbackChain?: FallbackEntry[]
|
fallbackChain?: FallbackEntry[]
|
||||||
availableModels: Set<string>
|
availableModels: Set<string>
|
||||||
@ -43,16 +44,23 @@ export function resolveModel(input: ModelResolutionInput): string | undefined {
|
|||||||
export function resolveModelWithFallback(
|
export function resolveModelWithFallback(
|
||||||
input: ExtendedModelResolutionInput,
|
input: ExtendedModelResolutionInput,
|
||||||
): ModelResolutionResult | undefined {
|
): ModelResolutionResult | undefined {
|
||||||
const { userModel, fallbackChain, availableModels, systemDefaultModel } = input
|
const { uiSelectedModel, userModel, fallbackChain, availableModels, systemDefaultModel } = input
|
||||||
|
|
||||||
// Step 1: Override
|
// Step 1: UI Selection (highest priority - respects user's model choice in OpenCode UI)
|
||||||
|
const normalizedUiModel = normalizeModel(uiSelectedModel)
|
||||||
|
if (normalizedUiModel) {
|
||||||
|
log("Model resolved via UI selection", { model: normalizedUiModel })
|
||||||
|
return { model: normalizedUiModel, source: "override" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Config Override (from oh-my-opencode.json)
|
||||||
const normalizedUserModel = normalizeModel(userModel)
|
const normalizedUserModel = normalizeModel(userModel)
|
||||||
if (normalizedUserModel) {
|
if (normalizedUserModel) {
|
||||||
log("Model resolved via override", { model: normalizedUserModel })
|
log("Model resolved via config override", { model: normalizedUserModel })
|
||||||
return { model: normalizedUserModel, source: "override" }
|
return { model: normalizedUserModel, source: "override" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Provider fallback chain (with availability check)
|
// Step 3: Provider fallback chain (with availability check)
|
||||||
if (fallbackChain && fallbackChain.length > 0) {
|
if (fallbackChain && fallbackChain.length > 0) {
|
||||||
if (availableModels.size === 0) {
|
if (availableModels.size === 0) {
|
||||||
const connectedProviders = readConnectedProvidersCache()
|
const connectedProviders = readConnectedProvidersCache()
|
||||||
@ -91,7 +99,7 @@ export function resolveModelWithFallback(
|
|||||||
log("No available model found in fallback chain, falling through to system default")
|
log("No available model found in fallback chain, falling through to system default")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: System default (if provided)
|
// Step 4: System default (if provided)
|
||||||
if (systemDefaultModel === undefined) {
|
if (systemDefaultModel === undefined) {
|
||||||
log("No model resolved - systemDefaultModel not configured")
|
log("No model resolved - systemDefaultModel not configured")
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user