Compare commits

..

1 Commits

Author SHA1 Message Date
YeonGyu-Kim
9ca259dcdc fix(runtime-fallback): preserve agent variant and reasoningEffort on model fallback (fixes #2621)
When runtime fallback switches to a different model, the agent's
configured variant and reasoningEffort were lost because
buildRetryModelPayload only extracted variant from the fallback
model string itself.

Now buildRetryModelPayload accepts optional agentSettings and uses
the agent's variant as fallback when the model string doesn't
include one. reasoningEffort is also passed through.

🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 12:11:01 +09:00
8 changed files with 147 additions and 57 deletions

View File

@@ -9,11 +9,8 @@ import { loadAvailableModelsFromCache } from "./model-resolution-cache"
import { getModelResolutionInfoWithOverrides } from "./model-resolution"
import type { OmoConfig } from "./model-resolution-types"
const PACKAGE_NAME_ALT = "oh-my-openagent"
const USER_CONFIG_BASE = join(getOpenCodeConfigDir({ binary: "opencode" }), PACKAGE_NAME)
const USER_CONFIG_BASE_ALT = join(getOpenCodeConfigDir({ binary: "opencode" }), PACKAGE_NAME_ALT)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
const PROJECT_CONFIG_BASE_ALT = join(process.cwd(), ".opencode", PACKAGE_NAME_ALT)
interface ConfigValidationResult {
exists: boolean
@@ -27,15 +24,9 @@ function findConfigPath(): string | null {
const projectConfig = detectConfigFile(PROJECT_CONFIG_BASE)
if (projectConfig.format !== "none") return projectConfig.path
const projectConfigAlt = detectConfigFile(PROJECT_CONFIG_BASE_ALT)
if (projectConfigAlt.format !== "none") return projectConfigAlt.path
const userConfig = detectConfigFile(USER_CONFIG_BASE)
if (userConfig.format !== "none") return userConfig.path
const userConfigAlt = detectConfigFile(USER_CONFIG_BASE_ALT)
if (userConfigAlt.format !== "none") return userConfigAlt.path
return null
}

View File

@@ -101,7 +101,13 @@ export function createAutoRetryHelpers(deps: HookDeps) {
return
}
const retryModelPayload = buildRetryModelPayload(newModel)
const agentSettings = resolvedAgent
? pluginConfig?.agents?.[resolvedAgent as keyof typeof pluginConfig.agents]
: undefined
const retryModelPayload = buildRetryModelPayload(newModel, agentSettings ? {
variant: agentSettings.variant,
reasoningEffort: agentSettings.reasoningEffort,
} : undefined)
if (!retryModelPayload) {
log(`[${HOOK_NAME}] Invalid model format (missing provider prefix): ${newModel}`)
const state = sessionStates.get(sessionID)

View File

@@ -0,0 +1,114 @@
import { describe, test, expect } from "bun:test"
import { buildRetryModelPayload } from "./retry-model-payload"
describe("buildRetryModelPayload", () => {
test("should return undefined for empty model string", () => {
// given
const model = ""
// when
const result = buildRetryModelPayload(model)
// then
expect(result).toBeUndefined()
})
test("should return undefined for model without provider prefix", () => {
// given
const model = "kimi-k2.5"
// when
const result = buildRetryModelPayload(model)
// then
expect(result).toBeUndefined()
})
test("should parse provider and model ID", () => {
// given
const model = "chutes/kimi-k2.5"
// when
const result = buildRetryModelPayload(model)
// then
expect(result).toEqual({
model: { providerID: "chutes", modelID: "kimi-k2.5" },
})
})
test("should include variant from model string", () => {
// given
const model = "anthropic/claude-sonnet-4-5 high"
// when
const result = buildRetryModelPayload(model)
// then
expect(result).toEqual({
model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" },
variant: "high",
})
})
test("should use agent variant when model string has no variant", () => {
// given
const model = "chutes/kimi-k2.5"
const agentSettings = { variant: "max" }
// when
const result = buildRetryModelPayload(model, agentSettings)
// then
expect(result).toEqual({
model: { providerID: "chutes", modelID: "kimi-k2.5" },
variant: "max",
})
})
test("should prefer model string variant over agent variant", () => {
// given
const model = "anthropic/claude-sonnet-4-5 high"
const agentSettings = { variant: "max" }
// when
const result = buildRetryModelPayload(model, agentSettings)
// then
expect(result).toEqual({
model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" },
variant: "high",
})
})
test("should include reasoningEffort from agent settings", () => {
// given
const model = "openai/gpt-5.4"
const agentSettings = { variant: "high", reasoningEffort: "xhigh" }
// when
const result = buildRetryModelPayload(model, agentSettings)
// then
expect(result).toEqual({
model: { providerID: "openai", modelID: "gpt-5.4" },
variant: "high",
reasoningEffort: "xhigh",
})
})
test("should not include reasoningEffort when agent settings has none", () => {
// given
const model = "chutes/kimi-k2.5"
const agentSettings = { variant: "medium" }
// when
const result = buildRetryModelPayload(model, agentSettings)
// then
expect(result).toEqual({
model: { providerID: "chutes", modelID: "kimi-k2.5" },
variant: "medium",
})
})
})

View File

@@ -2,24 +2,29 @@ import { parseModelString } from "../../tools/delegate-task/model-string-parser"
export function buildRetryModelPayload(
model: string,
): { model: { providerID: string; modelID: string }; variant?: string } | undefined {
agentSettings?: { variant?: string; reasoningEffort?: string },
): { model: { providerID: string; modelID: string }; variant?: string; reasoningEffort?: string } | undefined {
const parsedModel = parseModelString(model)
if (!parsedModel) {
return undefined
}
return parsedModel.variant
? {
model: {
providerID: parsedModel.providerID,
modelID: parsedModel.modelID,
},
variant: parsedModel.variant,
}
: {
model: {
providerID: parsedModel.providerID,
modelID: parsedModel.modelID,
},
}
const variant = parsedModel.variant ?? agentSettings?.variant
const reasoningEffort = agentSettings?.reasoningEffort
const payload: { model: { providerID: string; modelID: string }; variant?: string; reasoningEffort?: string } = {
model: {
providerID: parsedModel.providerID,
modelID: parsedModel.modelID,
},
}
if (variant) {
payload.variant = variant
}
if (reasoningEffort) {
payload.reasoningEffort = reasoningEffort
}
return payload
}

View File

@@ -160,32 +160,18 @@ export function loadPluginConfig(
directory: string,
ctx: unknown
): OhMyOpenCodeConfig {
// User-level config path - prefer .jsonc over .json, try oh-my-openagent as fallback
// User-level config path - prefer .jsonc over .json
const configDir = getOpenCodeConfigDir({ binary: "opencode" });
const userBasePath = path.join(configDir, "oh-my-opencode");
let userDetected = detectConfigFile(userBasePath);
if (userDetected.format === "none") {
const altUserBasePath = path.join(configDir, "oh-my-openagent");
const altDetected = detectConfigFile(altUserBasePath);
if (altDetected.format !== "none") {
userDetected = altDetected;
}
}
const userDetected = detectConfigFile(userBasePath);
const userConfigPath =
userDetected.format !== "none"
? userDetected.path
: userBasePath + ".json";
// Project-level config path - prefer .jsonc over .json, try oh-my-openagent as fallback
// Project-level config path - prefer .jsonc over .json
const projectBasePath = path.join(directory, ".opencode", "oh-my-opencode");
let projectDetected = detectConfigFile(projectBasePath);
if (projectDetected.format === "none") {
const altProjectBasePath = path.join(directory, ".opencode", "oh-my-openagent");
const altDetected = detectConfigFile(altProjectBasePath);
if (altDetected.format !== "none") {
projectDetected = altDetected;
}
}
const projectDetected = detectConfigFile(projectBasePath);
const projectConfigPath =
projectDetected.format !== "none"
? projectDetected.path

View File

@@ -12,5 +12,4 @@ export type OpenCodeConfigPaths = {
configJsonc: string
packageJson: string
omoConfig: string
omoConfigAlt: string
}

View File

@@ -84,7 +84,6 @@ export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenC
configJsonc: join(configDir, "opencode.jsonc"),
packageJson: join(configDir, "package.json"),
omoConfig: join(configDir, "oh-my-opencode.json"),
omoConfigAlt: join(configDir, "oh-my-openagent.json"),
}
}

View File

@@ -37,19 +37,9 @@ export function loadJsonFile<T>(path: string): T | null {
export function getConfigPaths(): { project: string; user: string; opencode: string } {
const cwd = process.cwd()
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
const projectDetected = detectConfigFile(join(cwd, ".opencode", "oh-my-opencode"))
const projectPath = projectDetected.format !== "none"
? projectDetected.path
: detectConfigFile(join(cwd, ".opencode", "oh-my-openagent")).path
const userDetected = detectConfigFile(join(configDir, "oh-my-opencode"))
const userPath = userDetected.format !== "none"
? userDetected.path
: detectConfigFile(join(configDir, "oh-my-openagent")).path
return {
project: projectPath,
user: userPath,
project: detectConfigFile(join(cwd, ".opencode", "oh-my-opencode")).path,
user: detectConfigFile(join(configDir, "oh-my-opencode")).path,
opencode: detectConfigFile(join(configDir, "opencode")).path,
}
}