Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/custom-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 29 additions & 6 deletions packages/server/src/server/agent/provider-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -872,11 +872,6 @@ describe("model merging", () => {
});

expect(models).toEqual([
{
provider: "claude",
id: "runtime-model",
label: "Runtime Model",
},
{
provider: "claude",
id: "shared-model",
Expand Down Expand Up @@ -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");
});
});
2 changes: 1 addition & 1 deletion packages/server/src/server/agent/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
49 changes: 29 additions & 20 deletions packages/server/src/server/agent/providers/claude/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
});
});

Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/server/agent/providers/claude/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ interface ClaudeAgentClientOptions {
runtimeSettings?: ProviderRuntimeSettings;
queryFactory?: ClaudeQueryFactory;
resolveBinary?: () => Promise<string>;
configDir?: string;
}

interface ClaudeAgentSessionOptions {
Expand Down Expand Up @@ -1280,13 +1281,15 @@ export class ClaudeAgentClient implements AgentClient {
private readonly runtimeSettings?: ProviderRuntimeSettings;
private readonly queryFactory?: ClaudeQueryFactory;
private readonly resolveBinary: () => Promise<string>;
private readonly configDir?: string;

constructor(options: ClaudeAgentClientOptions) {
this.defaults = options.defaults;
this.logger = options.logger.child({ module: "agent", provider: "claude" });
this.runtimeSettings = options.runtimeSettings;
this.queryFactory = options.queryFactory;
this.resolveBinary = options.resolveBinary ?? (() => resolveClaudeBinary(this.runtimeSettings));
this.configDir = options.configDir;
}

async createSession(
Expand Down Expand Up @@ -1337,7 +1340,7 @@ export class ClaudeAgentClient implements AgentClient {

async listModels(_options: ListModelsOptions): Promise<AgentModelDefinition[]> {
// 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<AgentFeature[]> {
Expand Down
18 changes: 12 additions & 6 deletions packages/server/src/server/agent/providers/claude/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,12 @@ export function getClaudeModels(): AgentModelDefinition[] {
return CLAUDE_MODELS.map((model) => ({ ...model }));
}

export async function getClaudeModelsWithSettings(logger: Logger): Promise<AgentModelDefinition[]> {
export async function getClaudeModelsWithSettings(
logger: Logger,
configDir?: string,
): Promise<AgentModelDefinition[]> {
const hardcodedModels = getClaudeModels();
const settingsModels = await readClaudeSettingsModels(logger);
const settingsModels = await readClaudeSettingsModels(logger, configDir);
if (settingsModels.length === 0) {
return hardcodedModels;
}
Expand All @@ -119,8 +122,11 @@ export async function getClaudeModelsWithSettings(logger: Logger): Promise<Agent
return models;
}

async function readClaudeSettingsModels(logger: Logger): Promise<AgentModelDefinition[]> {
const settingsPath = path.join(resolveClaudeConfigDir(), "settings.json");
async function readClaudeSettingsModels(
logger: Logger,
configDir?: string,
): Promise<AgentModelDefinition[]> {
const settingsPath = path.join(resolveClaudeConfigDir(configDir), "settings.json");

let parsed: unknown;
try {
Expand Down Expand Up @@ -155,8 +161,8 @@ async function readClaudeSettingsModels(logger: Logger): Promise<AgentModelDefin
return models;
}

function resolveClaudeConfigDir(): string {
return process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
function resolveClaudeConfigDir(configDir?: string): string {
return configDir ?? process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
}

function addSettingsModel(
Expand Down
Loading