mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(claude): respect profile models for built-in claude provider (#1311)
* fix(claude): respect profile models for built-in claude provider The built-in Claude provider was the only one in the registry whose profile `models` array was treated as additive on top of the hardcoded first-party catalog. Every other built-in provider (and every custom provider) replaces the runtime model list when `models` is set, which matches the documented behavior in docs/custom-providers.md. For users pointing Claude Code at a third-party Anthropic-compatible gateway (Z.AI, Alibaba/Qwen, MiniMax, custom proxies, …) and curating the picker with `agents.providers.claude.models`, the old behavior leaked the nine first-party Claude models into the dropdown, making it impossible to ship a curated list. This is issue #1299. The fix flips the built-in Claude entry in `provider-registry.ts` to match the other providers, so `models` replaces the runtime catalog (including the settings.json-discovered entries surfaced by getClaudeModelsWithSettings). The existing `additionalModels` field keeps its additive semantics for anyone who still wants to append entries on top of the first-party list. - provider-registry.ts: drop the claude-only `profileModelsAreAdditive` branch and align with the rest of the registry. - provider-registry.test.ts: update the "append to runtime models" expectation for built-in Claude to "replace runtime models" and add a regression test that mirrors the issue scenario (hardcoded catalog + configured profile models, expect only the profile models). - agent.test.ts: make the "returns hardcoded claude models" hermetic by pointing CLAUDE_CONFIG_DIR at an empty temp dir; the test was reading the host's real ~/.claude/settings.json (which now contains MiniMax env vars from the issue report) and leaking that into the assertion. - custom-providers.md: correct the note about Claude profile models being additive and document the new replace semantics alongside additionalModels. Closes #1299 * test(claude): drop explanatory comments from new tests * refactor(claude): inject configDir into ClaudeAgentClient for test hermeticity Greptile flagged the previous hermeticity fix on agent.test.ts: mutating process.env.CLAUDE_CONFIG_DIR inside a test leaks the redirection into any concurrent test in the same process for the duration of the try block. Thread a `configDir` option through ClaudeAgentClient -> getClaudeModelsWithSettings -> readClaudeSettingsModels -> resolveClaudeConfigDir instead. The constructor follows the same injection pattern as `resolveBinary`, so the test passes an empty temp dir via the option and no shared global state is touched. - models.ts: add an optional `configDir` to getClaudeModelsWithSettings, readClaudeSettingsModels, and resolveClaudeConfigDir. Env var remains the fallback for production callers. - agent.ts: add `configDir?` to ClaudeAgentClientOptions, store it on the instance, and forward it to getClaudeModelsWithSettings from listModels. - agent.test.ts: drop the process.env save/mutate/restore dance and pass `configDir: emptyConfigDir` to the constructor instead. Refs #1311
This commit is contained in:
committed by
GitHub
parent
350bc08fc4
commit
1377adbece
@@ -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
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 });
|
||||
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",
|
||||
]);
|
||||
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");
|
||||
} finally {
|
||||
await fs.rm(emptyConfigDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const defaultModel = models.find((m) => m.isDefault);
|
||||
expect(defaultModel?.id).toBe("claude-opus-4-8");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -279,6 +279,7 @@ interface ClaudeAgentClientOptions {
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
queryFactory?: ClaudeQueryFactory;
|
||||
resolveBinary?: () => Promise<string>;
|
||||
configDir?: string;
|
||||
}
|
||||
|
||||
interface ClaudeAgentSessionOptions {
|
||||
@@ -1277,6 +1278,7 @@ 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;
|
||||
@@ -1284,6 +1286,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(
|
||||
@@ -1334,7 +1337,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[]> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user