diff --git a/src/shared/model-format-normalizer.test.ts b/src/shared/model-format-normalizer.test.ts new file mode 100644 index 000000000..d28ab975b --- /dev/null +++ b/src/shared/model-format-normalizer.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from "bun:test" +import { normalizeModelFormat } from "./model-format-normalizer" + +describe("normalizeModelFormat", () => { + describe("string format input", () => { + it("splits provider/model format correctly", () => { + const result = normalizeModelFormat("opencode/glm-5-free") + expect(result).toEqual({ providerID: "opencode", modelID: "glm-5-free" }) + }) + + it("handles provider with multiple slashes", () => { + const result = normalizeModelFormat("anthropic/claude-opus-4-6/max") + expect(result).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6/max" }) + }) + + it("returns undefined for malformed string without separator", () => { + const result = normalizeModelFormat("invalid") + expect(result).toBeUndefined() + }) + + it("returns undefined for empty string", () => { + const result = normalizeModelFormat("") + expect(result).toBeUndefined() + }) + }) + + describe("object format input", () => { + it("passthroughs object format unchanged", () => { + const input = { providerID: "opencode", modelID: "glm-5-free" } + const result = normalizeModelFormat(input) + expect(result).toEqual(input) + }) + }) + + describe("edge cases", () => { + it("returns undefined for null", () => { + const result = normalizeModelFormat(null) + expect(result).toBeUndefined() + }) + + it("returns undefined for undefined", () => { + const result = normalizeModelFormat(undefined) + expect(result).toBeUndefined() + }) + }) +}) diff --git a/src/shared/model-format-normalizer.ts b/src/shared/model-format-normalizer.ts new file mode 100644 index 000000000..98d255f78 --- /dev/null +++ b/src/shared/model-format-normalizer.ts @@ -0,0 +1,20 @@ +export function normalizeModelFormat( + model: string | { providerID: string; modelID: string } +): { providerID: string; modelID: string } | undefined { + if (!model) { + return undefined + } + + if (typeof model === "object" && "providerID" in model && "modelID" in model) { + return { providerID: model.providerID, modelID: model.modelID } + } + + if (typeof model === "string") { + const parts = model.split("/") + if (parts.length >= 2) { + return { providerID: parts[0], modelID: parts.slice(1).join("/") } + } + } + + return undefined +} diff --git a/src/shared/model-resolution-pipeline.ts b/src/shared/model-resolution-pipeline.ts index c51cad371..8eb12e5c5 100644 --- a/src/shared/model-resolution-pipeline.ts +++ b/src/shared/model-resolution-pipeline.ts @@ -34,6 +34,7 @@ export type ModelResolutionResult = { variant?: string attempted?: string[] reason?: string + explicitUserConfig?: boolean } @@ -55,7 +56,7 @@ export function resolveModelPipeline( const normalizedUserModel = normalizeModel(intent?.userModel) if (normalizedUserModel) { log("Model resolved via config override", { model: normalizedUserModel }) - return { model: normalizedUserModel, provenance: "override" } + return { model: normalizedUserModel, provenance: "override", explicitUserConfig: true } } const normalizedCategoryDefault = normalizeModel(intent?.categoryDefaultModel) diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index 043243db2..1d0e65db6 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -2,7 +2,7 @@ import type { DelegateTaskArgs } from "./types" import type { ExecutorContext } from "./executor-types" import { isPlanFamily } from "./constants" import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent" -import { parseModelString } from "./model-string-parser" +import { normalizeModelFormat } from "../../shared/model-format-normalizer" import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements" import { getAgentDisplayName, getAgentConfigKey } from "../../shared/agent-display-names" import { normalizeSDKResponse } from "../../shared" @@ -99,8 +99,9 @@ Create the work plan directly - that's your job as the planning agent.`, if (agentOverride?.model || agentRequirement || matchedAgent.model) { const availableModels = await getAvailableModelsForDelegateTask(client) - const matchedAgentModelStr = matchedAgent.model - ? `${matchedAgent.model.providerID}/${matchedAgent.model.modelID}` + const normalizedMatchedModel = normalizeModelFormat(matchedAgent.model as Parameters[0]) + const matchedAgentModelStr = normalizedMatchedModel + ? `${normalizedMatchedModel.providerID}/${normalizedMatchedModel.modelID}` : undefined const resolution = resolveModelForDelegateTask({ @@ -112,10 +113,10 @@ Create the work plan directly - that's your job as the planning agent.`, }) if (resolution) { - const parsed = parseModelString(resolution.model) - if (parsed) { + const normalized = normalizeModelFormat(resolution.model) + if (normalized) { const variantToUse = agentOverride?.variant ?? resolution.variant - categoryModel = variantToUse ? { ...parsed, variant: variantToUse } : parsed + categoryModel = variantToUse ? { ...normalized, variant: variantToUse } : normalized } } }