diff --git a/docs/custom-providers.md b/docs/custom-providers.md index 5ae98afb2..69ed91212 100644 --- a/docs/custom-providers.md +++ b/docs/custom-providers.md @@ -577,7 +577,7 @@ Each entry in the `models` array: The built-in `claude` provider appends concrete model IDs from `~/.claude/settings.json` to its first-party Claude model list. Paseo reads the top-level `model` field and these `env` keys: `ANTHROPIC_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and `ANTHROPIC_DEFAULT_HAIKU_MODEL`. -This lets users who already configured Claude Code for Bedrock, OpenRouter, ollama, Z.AI, or another Anthropic-compatible gateway select the exact model ID in Paseo. `agents.providers.claude.models` is still supported and is additive for the built-in Claude provider; duplicate IDs are de-duplicated. +This lets users who already configured Claude Code for Bedrock, OpenRouter, ollama, Z.AI, or another Anthropic-compatible gateway select the exact model ID in Paseo. When `agents.providers.claude.models` is set it **replaces** both the hardcoded first-party Claude list and any settings.json-discovered entries; use `agents.providers.claude.additionalModels` to keep the first-party list and append curated entries on top. ### Gotcha: `extends: "claude"` with third-party endpoints diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index 9d8acc8ae..565ca6999 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -835,7 +835,7 @@ describe("model merging", () => { ]); }); - test("built-in Claude profile models append to runtime models", async () => { + test("built-in Claude profile models replace runtime models (issue #1299)", async () => { mockState.runtimeModels.set("claude", [ { provider: "claude", @@ -872,11 +872,6 @@ describe("model merging", () => { }); expect(models).toEqual([ - { - provider: "claude", - id: "runtime-model", - label: "Runtime Model", - }, { provider: "claude", id: "shared-model", @@ -1129,4 +1124,32 @@ describe("model merging", () => { const defaultModel = models.find((model) => model.isDefault) ?? models[0]; expect(defaultModel?.id).toBe("profile-default"); }); + + test("built-in Claude models override replaces hardcoded first-party models (issue #1299)", async () => { + mockState.runtimeModels.set("claude", [ + { provider: "claude", id: "claude-opus-4-8", label: "Opus 4.8", isDefault: true }, + { provider: "claude", id: "claude-opus-4-7", label: "Opus 4.7" }, + { provider: "claude", id: "claude-sonnet-4-6", label: "Sonnet 4.6" }, + { provider: "claude", id: "claude-haiku-4-5", label: "Haiku 4.5" }, + ]); + + const registry = buildProviderRegistry(logger, { + providerOverrides: { + claude: { + models: [ + { id: "MiniMax-M2.7", label: "MiniMax-M2.7" }, + { id: "MiniMax-M3", label: "MiniMax-M3", isDefault: true }, + ], + }, + }, + }); + + const models = await registry.claude.fetchModels({ + cwd: "/tmp/registry-models", + force: false, + }); + + expect(models.map((model) => model.id)).toEqual(["MiniMax-M2.7", "MiniMax-M3"]); + expect(models.find((model) => model.isDefault)?.id).toBe("MiniMax-M3"); + }); }); diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index b474047fe..823ea6546 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -523,7 +523,7 @@ function buildResolvedBuiltinProviders( runtimeSettings: mergedRuntimeSettings, profileModels: override?.models ?? [], additionalModels: override?.additionalModels ?? [], - profileModelsAreAdditive: definition.id === "claude", + profileModelsAreAdditive: false, enabled: override?.enabled !== false, derivedFromProviderId: null, createBaseClient: (logger) => diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index be905bf9e..382885e8e 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -397,28 +397,37 @@ describe("ClaudeAgentClient.listModels", () => { const logger = createTestLogger(); test("returns hardcoded claude models", async () => { - const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); - const models = await client.listModels({ cwd: "/tmp/claude-models", force: false }); - - expect(models.map((m) => m.id)).toEqual([ - "claude-opus-4-8[1m]", - "claude-opus-4-8", - "claude-opus-4-7[1m]", - "claude-opus-4-7", - "claude-opus-4-6[1m]", - "claude-opus-4-6", - "claude-sonnet-4-6[1m]", - "claude-sonnet-4-6", - "claude-haiku-4-5", - ]); + const emptyConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-empty-")); + try { + const client = new ClaudeAgentClient({ + logger, + resolveBinary: async () => "/test/claude/bin", + configDir: emptyConfigDir, + }); + const models = await client.listModels({ cwd: "/tmp/claude-models", force: false }); + + expect(models.map((m) => m.id)).toEqual([ + "claude-opus-4-8[1m]", + "claude-opus-4-8", + "claude-opus-4-7[1m]", + "claude-opus-4-7", + "claude-opus-4-6[1m]", + "claude-opus-4-6", + "claude-sonnet-4-6[1m]", + "claude-sonnet-4-6", + "claude-haiku-4-5", + ]); - for (const model of models) { - expect(model.provider).toBe("claude"); - expect(model.label.length).toBeGreaterThan(0); - } + for (const model of models) { + expect(model.provider).toBe("claude"); + expect(model.label.length).toBeGreaterThan(0); + } - const defaultModel = models.find((m) => m.isDefault); - expect(defaultModel?.id).toBe("claude-opus-4-8"); + const defaultModel = models.find((m) => m.isDefault); + expect(defaultModel?.id).toBe("claude-opus-4-8"); + } finally { + await fs.rm(emptyConfigDir, { recursive: true, force: true }); + } }); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index d35aa1bd1..60a206448 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -278,6 +278,7 @@ interface ClaudeAgentClientOptions { runtimeSettings?: ProviderRuntimeSettings; queryFactory?: ClaudeQueryFactory; resolveBinary?: () => Promise; + configDir?: string; } interface ClaudeAgentSessionOptions { @@ -1280,6 +1281,7 @@ export class ClaudeAgentClient implements AgentClient { private readonly runtimeSettings?: ProviderRuntimeSettings; private readonly queryFactory?: ClaudeQueryFactory; private readonly resolveBinary: () => Promise; + private readonly configDir?: string; constructor(options: ClaudeAgentClientOptions) { this.defaults = options.defaults; @@ -1287,6 +1289,7 @@ export class ClaudeAgentClient implements AgentClient { this.runtimeSettings = options.runtimeSettings; this.queryFactory = options.queryFactory; this.resolveBinary = options.resolveBinary ?? (() => resolveClaudeBinary(this.runtimeSettings)); + this.configDir = options.configDir; } async createSession( @@ -1337,7 +1340,7 @@ export class ClaudeAgentClient implements AgentClient { async listModels(_options: ListModelsOptions): Promise { // Claude exposes a global catalog here; cwd/force are intentionally irrelevant. - return await getClaudeModelsWithSettings(this.logger); + return await getClaudeModelsWithSettings(this.logger, this.configDir); } async listFeatures(config: AgentSessionConfig): Promise { diff --git a/packages/server/src/server/agent/providers/claude/models.ts b/packages/server/src/server/agent/providers/claude/models.ts index 644d26ff6..31c2cc77f 100644 --- a/packages/server/src/server/agent/providers/claude/models.ts +++ b/packages/server/src/server/agent/providers/claude/models.ts @@ -98,9 +98,12 @@ export function getClaudeModels(): AgentModelDefinition[] { return CLAUDE_MODELS.map((model) => ({ ...model })); } -export async function getClaudeModelsWithSettings(logger: Logger): Promise { +export async function getClaudeModelsWithSettings( + logger: Logger, + configDir?: string, +): Promise { const hardcodedModels = getClaudeModels(); - const settingsModels = await readClaudeSettingsModels(logger); + const settingsModels = await readClaudeSettingsModels(logger, configDir); if (settingsModels.length === 0) { return hardcodedModels; } @@ -119,8 +122,11 @@ export async function getClaudeModelsWithSettings(logger: Logger): Promise { - const settingsPath = path.join(resolveClaudeConfigDir(), "settings.json"); +async function readClaudeSettingsModels( + logger: Logger, + configDir?: string, +): Promise { + const settingsPath = path.join(resolveClaudeConfigDir(configDir), "settings.json"); let parsed: unknown; try { @@ -155,8 +161,8 @@ async function readClaudeSettingsModels(logger: Logger): Promise