From fee16a70c32969527560596cdf76cdd11a364933 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:10:03 +0200 Subject: [PATCH] fix(cli): preserve anthropic api provider --- src/cli.ts | 13 ++++- src/provider-migrations.ts | 34 ++++++++++++ src/tests/provider-migrations.test.ts | 77 +++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/provider-migrations.ts create mode 100644 src/tests/provider-migrations.test.ts diff --git a/src/cli.ts b/src/cli.ts index 5009f23b7..24a6c740d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,6 +18,7 @@ import { ensureManagedTools } from './tool-bootstrap.js' import { loadStoredEnvKeys } from './wizard.js' import { migratePiCredentials } from './pi-migration.js' import { validateConfiguredModel } from './startup-model-validation.js' +import { shouldMigrateAnthropicToClaudeCode } from './provider-migrations.js' import { shouldRunOnboarding, runOnboarding } from './onboarding.js' import chalk from 'chalk' import { checkForUpdates } from './update-check.js' @@ -470,7 +471,11 @@ if (isPrintMode) { // 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') { + if (shouldMigrateAnthropicToClaudeCode({ + authStorage, + isClaudeCodeReady: modelRegistry.isProviderRequestReady('claude-code'), + defaultProvider: settingsManager.getDefaultProvider(), + })) { const currentModelId = settingsManager.getDefaultModel() if (currentModelId) { const ccModel = modelRegistry.find('claude-code', currentModelId) @@ -662,7 +667,11 @@ 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') { +if (shouldMigrateAnthropicToClaudeCode({ + authStorage, + isClaudeCodeReady: modelRegistry.isProviderRequestReady('claude-code'), + defaultProvider: settingsManager.getDefaultProvider(), +})) { const currentModelId = settingsManager.getDefaultModel() if (currentModelId) { const ccModel = modelRegistry.find('claude-code', currentModelId) diff --git a/src/provider-migrations.ts b/src/provider-migrations.ts new file mode 100644 index 000000000..1e61c69df --- /dev/null +++ b/src/provider-migrations.ts @@ -0,0 +1,34 @@ +import type { AuthStorage } from "@gsd/pi-coding-agent" + +type AnthropicMigrationDeps = { + authStorage: Pick + isClaudeCodeReady: boolean + defaultProvider: string | undefined + env?: NodeJS.ProcessEnv +} + +export function hasDirectAnthropicApiKey( + authStorage: Pick, + env: NodeJS.ProcessEnv = process.env, +): boolean { + if ((env.ANTHROPIC_API_KEY ?? "").trim()) { + return true + } + + return authStorage.getCredentialsForProvider("anthropic").some((credential: any) => + credential?.type === "api_key" && typeof credential?.key === "string" && credential.key.trim().length > 0, + ) +} + +export function shouldMigrateAnthropicToClaudeCode({ + authStorage, + isClaudeCodeReady, + defaultProvider, + env = process.env, +}: AnthropicMigrationDeps): boolean { + if (!isClaudeCodeReady || defaultProvider !== "anthropic") { + return false + } + + return !hasDirectAnthropicApiKey(authStorage, env) +} diff --git a/src/tests/provider-migrations.test.ts b/src/tests/provider-migrations.test.ts new file mode 100644 index 000000000..3aef11e72 --- /dev/null +++ b/src/tests/provider-migrations.test.ts @@ -0,0 +1,77 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { hasDirectAnthropicApiKey, shouldMigrateAnthropicToClaudeCode } from "../provider-migrations.ts" + +function makeAuthStorage(credentials: unknown[]) { + return { + getCredentialsForProvider(provider: string) { + return provider === "anthropic" ? credentials : [] + }, + } +} + +test("hasDirectAnthropicApiKey detects non-empty auth storage keys", () => { + assert.equal( + hasDirectAnthropicApiKey( + makeAuthStorage([{ type: "api_key", key: "sk-ant-test" }]) as any, + {} as NodeJS.ProcessEnv, + ), + true, + ) +}) + +test("hasDirectAnthropicApiKey ignores empty placeholder keys", () => { + assert.equal( + hasDirectAnthropicApiKey( + makeAuthStorage([{ type: "api_key", key: "" }]) as any, + {} as NodeJS.ProcessEnv, + ), + false, + ) +}) + +test("hasDirectAnthropicApiKey detects ANTHROPIC_API_KEY env fallback", () => { + assert.equal( + hasDirectAnthropicApiKey( + makeAuthStorage([]) as any, + { ANTHROPIC_API_KEY: "sk-ant-env" } as NodeJS.ProcessEnv, + ), + true, + ) +}) + +test("shouldMigrateAnthropicToClaudeCode blocks migration for direct-key users", () => { + assert.equal( + shouldMigrateAnthropicToClaudeCode({ + authStorage: makeAuthStorage([{ type: "api_key", key: "sk-ant-test" }]) as any, + isClaudeCodeReady: true, + defaultProvider: "anthropic", + env: {} as NodeJS.ProcessEnv, + }), + false, + ) +}) + +test("shouldMigrateAnthropicToClaudeCode allows OAuth-only anthropic users", () => { + assert.equal( + shouldMigrateAnthropicToClaudeCode({ + authStorage: makeAuthStorage([{ type: "oauth", accessToken: "oauth-token" }]) as any, + isClaudeCodeReady: true, + defaultProvider: "anthropic", + env: {} as NodeJS.ProcessEnv, + }), + true, + ) +}) + +test("shouldMigrateAnthropicToClaudeCode stays off for other providers", () => { + assert.equal( + shouldMigrateAnthropicToClaudeCode({ + authStorage: makeAuthStorage([{ type: "oauth", accessToken: "oauth-token" }]) as any, + isClaudeCodeReady: true, + defaultProvider: "openai", + env: {} as NodeJS.ProcessEnv, + }), + false, + ) +})