Make model selection model-agnostic
Remove hard-coded Anthropic/Claude defaults and silent provider swaps so the app honors whatever model/provider the user has configured. - src/cli.ts: drop the anthropic->claude-code auto-migration blocks that were rewriting the user's saved defaultProvider on every startup. - packages/pi-coding-agent/src/core/model-resolver.ts: delete the defaultModelPerProvider table, drop the "recommended variant" swap that silently upgraded e.g. claude-opus-4-6 to -extended, and replace the provider-iteration first-available fallback with provider-sticky (user's saved provider first, then first registry entry). - src/startup-model-validation.ts: replace the openai/anthropic-first fallback chain with Pi-default -> same-provider -> first-available. - src/help-text.ts: use a generic provider/model-id example for --model instead of claude-opus-4-6. - src/tests/startup-model-validation.test.ts: update the fallback test to assert provider stickiness rather than a specific Claude model id. https://claude.ai/code/session_01CvuUuzuVjRcQN25263nG6V
This commit is contained in:
parent
804f1d4b94
commit
0ed576ac00
5 changed files with 31 additions and 106 deletions
|
|
@ -3,44 +3,13 @@
|
|||
*/
|
||||
|
||||
import type { ThinkingLevel } from "@gsd/pi-agent-core";
|
||||
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@gsd/pi-ai";
|
||||
import { type Api, type Model, modelsAreEqual } from "@gsd/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { minimatch } from "minimatch";
|
||||
import { isValidThinkingLevel } from "../cli/args.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import type { ModelRegistry } from "./model-registry.js";
|
||||
|
||||
/** Default model IDs for each known provider */
|
||||
const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
"amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
|
||||
anthropic: "claude-opus-4-6",
|
||||
"anthropic-vertex": "claude-sonnet-4-6",
|
||||
openai: "gpt-5.4",
|
||||
"azure-openai-responses": "gpt-5.2",
|
||||
"openai-codex": "gpt-5.4",
|
||||
google: "gemini-2.5-pro",
|
||||
"google-gemini-cli": "gemini-2.5-pro",
|
||||
"google-antigravity": "gemini-3.1-pro-high",
|
||||
"google-vertex": "gemini-3-pro-preview",
|
||||
"github-copilot": "gpt-4o",
|
||||
openrouter: "openai/gpt-5.1-codex",
|
||||
"vercel-ai-gateway": "anthropic/claude-opus-4-6",
|
||||
xai: "grok-4-fast-non-reasoning",
|
||||
groq: "openai/gpt-oss-120b",
|
||||
cerebras: "zai-glm-4.6",
|
||||
zai: "glm-4.6",
|
||||
mistral: "devstral-medium-latest",
|
||||
minimax: "MiniMax-M2.1",
|
||||
"minimax-cn": "MiniMax-M2.1",
|
||||
huggingface: "moonshotai/Kimi-K2.5",
|
||||
opencode: "claude-opus-4-6",
|
||||
"opencode-go": "kimi-k2.5",
|
||||
"kimi-coding": "kimi-k2-thinking",
|
||||
"alibaba-coding-plan": "qwen3.5-plus",
|
||||
ollama: "llama3.1:8b",
|
||||
"ollama-cloud": "qwen3:32b",
|
||||
};
|
||||
|
||||
export interface ScopedModel {
|
||||
model: Model<Api>;
|
||||
/** Thinking level if explicitly specified in pattern (e.g., "model:high"), undefined otherwise */
|
||||
|
|
@ -122,10 +91,11 @@ function buildFallbackModel(provider: string, modelId: string, availableModels:
|
|||
const providerModels = availableModels.filter((m) => m.provider === provider);
|
||||
if (providerModels.length === 0) return undefined;
|
||||
|
||||
const defaultId = defaultModelPerProvider[provider as KnownProvider];
|
||||
const baseModel = defaultId
|
||||
? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0])
|
||||
: providerModels[0];
|
||||
// Use the first available model from this provider as a template for
|
||||
// capabilities (context window, reasoning support, etc.). The user is
|
||||
// explicitly providing a custom model id, so we just need any shape of
|
||||
// model from the same provider to inherit from.
|
||||
const baseModel = providerModels[0];
|
||||
|
||||
return {
|
||||
...baseModel,
|
||||
|
|
@ -502,24 +472,11 @@ export async function findInitialModel(options: {
|
|||
};
|
||||
}
|
||||
|
||||
// 3. Try saved default from settings
|
||||
// 3. Try saved default from settings — use it exactly as configured.
|
||||
// Whatever the user chose is what gets used; no silent substitution.
|
||||
if (defaultProvider && defaultModelId) {
|
||||
const found = modelRegistry.find(defaultProvider, defaultModelId);
|
||||
if (found) {
|
||||
// Check if the provider's recommended default is a higher-capability variant
|
||||
// of the saved model (e.g. saved "claude-opus-4-6" vs recommended "claude-opus-4-6-extended").
|
||||
// If so, prefer the recommended variant to avoid using a smaller context window (#1125).
|
||||
const recommendedId = defaultModelPerProvider[defaultProvider as KnownProvider];
|
||||
if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) {
|
||||
const recommended = modelRegistry.find(defaultProvider, recommendedId);
|
||||
if (recommended) {
|
||||
model = recommended;
|
||||
if (defaultThinkingLevel) {
|
||||
thinkingLevel = defaultThinkingLevel;
|
||||
}
|
||||
return { model, thinkingLevel, fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
model = found;
|
||||
if (defaultThinkingLevel) {
|
||||
thinkingLevel = defaultThinkingLevel;
|
||||
|
|
@ -532,16 +489,17 @@ export async function findInitialModel(options: {
|
|||
const availableModels = await modelRegistry.getAvailable();
|
||||
|
||||
if (availableModels.length > 0) {
|
||||
// Try to find a default model from known providers
|
||||
for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
|
||||
const defaultId = defaultModelPerProvider[provider];
|
||||
const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
|
||||
if (match) {
|
||||
return { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
|
||||
// Prefer a model from the user's saved provider if any is still available —
|
||||
// provider stickiness, not a hard-coded Anthropic/OpenAI preference.
|
||||
if (defaultProvider) {
|
||||
const sameProvider = availableModels.find((m) => m.provider === defaultProvider);
|
||||
if (sameProvider) {
|
||||
return { model: sameProvider, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
// If no default found, use first available
|
||||
// Otherwise use the first available — registry order reflects models.json
|
||||
// order, which the user controls.
|
||||
return { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
|
|
|
|||
38
src/cli.ts
38
src/cli.ts
|
|
@ -467,25 +467,6 @@ if (isPrintMode) {
|
|||
})
|
||||
markStartup('createAgentSession')
|
||||
|
||||
// Migrate anthropic OAuth users to claude-code provider when CLI is available (#3772).
|
||||
// Anthropic blocks third-party apps from using subscription quotas — routing through
|
||||
// the local claude CLI binary is TOS-compliant.
|
||||
if (modelRegistry.isProviderRequestReady('claude-code') && settingsManager.getDefaultProvider() === 'anthropic') {
|
||||
const currentModelId = settingsManager.getDefaultModel()
|
||||
if (currentModelId) {
|
||||
const ccModel = modelRegistry.find('claude-code', currentModelId)
|
||||
if (ccModel) {
|
||||
try {
|
||||
await session.setModel(ccModel)
|
||||
// Only persist after successful session switch to avoid desync
|
||||
settingsManager.setDefaultModelAndProvider('claude-code', currentModelId)
|
||||
} catch {
|
||||
// claude-code provider not ready — leave both session and settings unchanged
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configured model AFTER extensions have registered their models (#2626).
|
||||
// Before this, extension-provided models (e.g. claude-code/*) were not yet in the
|
||||
// registry, causing the user's valid choice to be silently overwritten.
|
||||
|
|
@ -659,25 +640,6 @@ const { session, extensionsResult, modelFallbackMessage: interactiveFallbackMsg
|
|||
})
|
||||
markStartup('createAgentSession')
|
||||
|
||||
// Migrate anthropic OAuth users to claude-code provider when CLI is available (#3772).
|
||||
// Anthropic blocks third-party apps from using subscription quotas — routing through
|
||||
// the local claude CLI binary is TOS-compliant.
|
||||
if (modelRegistry.isProviderRequestReady('claude-code') && settingsManager.getDefaultProvider() === 'anthropic') {
|
||||
const currentModelId = settingsManager.getDefaultModel()
|
||||
if (currentModelId) {
|
||||
const ccModel = modelRegistry.find('claude-code', currentModelId)
|
||||
if (ccModel) {
|
||||
try {
|
||||
await session.setModel(ccModel)
|
||||
// Only persist after successful session switch to avoid desync
|
||||
settingsManager.setDefaultModelAndProvider('claude-code', currentModelId)
|
||||
} catch {
|
||||
// claude-code provider not ready — leave both session and settings unchanged
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configured model AFTER extensions have registered their models (#2626).
|
||||
// Before this, extension-provided models (e.g. claude-code/*) were not yet in the
|
||||
// registry, causing the user's valid choice to be silently overwritten.
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export function printHelp(version: string): void {
|
|||
process.stdout.write(' --print, -p Single-shot print mode\n')
|
||||
process.stdout.write(' --continue, -c Resume the most recent session\n')
|
||||
process.stdout.write(' --worktree, -w [name] Start in an isolated worktree (auto-named if omitted)\n')
|
||||
process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n')
|
||||
process.stdout.write(' --model <id> Override model (e.g. provider/model-id)\n')
|
||||
process.stdout.write(' --no-session Disable session persistence\n')
|
||||
process.stdout.write(' --extension <path> Load additional extension\n')
|
||||
process.stdout.write(' --tools <a,b,c> Restrict available tools\n')
|
||||
|
|
|
|||
|
|
@ -56,16 +56,19 @@ export function validateConfiguredModel(
|
|||
if (!configuredModel || !configuredExists) {
|
||||
// Model not configured at all, or removed from registry — pick a fallback.
|
||||
// Only fires when the model is genuinely unknown (not just temporarily unavailable).
|
||||
//
|
||||
// Model-agnostic selection order:
|
||||
// 1. Pi migration default (preserves migration from ~/.pi install)
|
||||
// 2. Any model from the user's previously-chosen provider (provider stickiness)
|
||||
// 3. First available model in registry order (user-controlled via models.json)
|
||||
const piDefault = getPiDefaultModelAndProvider()
|
||||
const preferred =
|
||||
(piDefault
|
||||
? availableModels.find((m) => m.provider === piDefault.provider && m.id === piDefault.model)
|
||||
: undefined) ||
|
||||
availableModels.find((m) => m.provider === 'openai' && m.id === 'gpt-5.4') ||
|
||||
availableModels.find((m) => m.provider === 'openai') ||
|
||||
availableModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-opus-4-6') ||
|
||||
availableModels.find((m) => m.provider === 'anthropic' && m.id.includes('opus')) ||
|
||||
availableModels.find((m) => m.provider === 'anthropic') ||
|
||||
(configuredProvider
|
||||
? availableModels.find((m) => m.provider === configuredProvider)
|
||||
: undefined) ||
|
||||
availableModels[0]
|
||||
if (preferred) {
|
||||
settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id)
|
||||
|
|
|
|||
|
|
@ -78,8 +78,10 @@ describe("validateConfiguredModel — regression #3534", () => {
|
|||
assert.notEqual(settings._model, "claude-opus-4-6[1m]");
|
||||
});
|
||||
|
||||
it("does not fall back to google when anthropic models are available", () => {
|
||||
// Simulate: stale setting triggers fallback, anthropic should be preferred over google
|
||||
it("prefers the user's saved provider when falling back", () => {
|
||||
// Simulate: stale model triggers fallback. The fallback should stay on
|
||||
// the user's chosen provider rather than silently jumping to a different
|
||||
// one — model-agnostic provider stickiness, not a hard-coded preference.
|
||||
const registry = createMockRegistry([
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
{ provider: "google", id: "gemini-2.5-pro" },
|
||||
|
|
@ -88,9 +90,9 @@ describe("validateConfiguredModel — regression #3534", () => {
|
|||
|
||||
validateConfiguredModel(registry, settings);
|
||||
|
||||
// Should pick anthropic fallback, not google
|
||||
// Provider stickiness: should stay on anthropic, since a model from
|
||||
// that provider is still available.
|
||||
assert.equal(settings._provider, "anthropic");
|
||||
assert.equal(settings._model, "claude-opus-4-6");
|
||||
});
|
||||
|
||||
it("resets thinking level when model is replaced", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue