diff --git a/src/resources/extensions/gsd/doctor-providers.ts b/src/resources/extensions/gsd/doctor-providers.ts index e0f35341b..06242fc81 100644 --- a/src/resources/extensions/gsd/doctor-providers.ts +++ b/src/resources/extensions/gsd/doctor-providers.ts @@ -185,11 +185,35 @@ const PROVIDER_ROUTES: Record = { google: ["google-gemini-cli"], }; +/** + * Providers that use external CLI authentication (not API keys). + * These are always considered "ok" — the host CLI handles auth. + */ +const CLI_AUTH_PROVIDERS = new Set([ + "claude-code", + "openai-codex", + "google-gemini-cli", + "google-antigravity", +]); + function checkLlmProviders(): ProviderCheckResult[] { const required = collectConfiguredModelProviders(); const results: ProviderCheckResult[] = []; for (const providerId of required) { + // CLI-authenticated providers don't need API keys — skip key check + if (CLI_AUTH_PROVIDERS.has(providerId)) { + const info = PROVIDER_REGISTRY.find(p => p.id === providerId); + results.push({ + name: providerId, + label: info?.label ?? providerId, + category: "llm", + status: "ok", + message: `${info?.label ?? providerId} — CLI auth (no key needed)`, + required: true, + }); + continue; + } const info = PROVIDER_REGISTRY.find(p => p.id === providerId); const label = providerId === "anthropic-vertex" ? "Anthropic Vertex" diff --git a/src/resources/extensions/gsd/tests/doctor-providers.test.ts b/src/resources/extensions/gsd/tests/doctor-providers.test.ts index 8df31fc10..3fee92d75 100644 --- a/src/resources/extensions/gsd/tests/doctor-providers.test.ts +++ b/src/resources/extensions/gsd/tests/doctor-providers.test.ts @@ -574,6 +574,42 @@ test("runProviderChecks reports ok for OpenAI via openai-codex auth.json (#2922) rmSync(tmpHome, { recursive: true, force: true }); }); +test("runProviderChecks reports ok for claude-code without any API key", () => { + const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-repo-"))); + mkdirSync(join(repo, ".gsd"), { recursive: true }); + writeFileSync( + join(repo, ".gsd", "PREFERENCES.md"), + [ + "---", + "models:", + " execution:", + " model: claude-sonnet-4-6", + " provider: claude-code", + "---", + "", + ].join("\n"), + ); + + const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-home-"))); + + withEnv({ + HOME: tmpHome, + ANTHROPIC_API_KEY: undefined, + ANTHROPIC_OAUTH_TOKEN: undefined, + }, () => { + withCwd(repo, () => { + const results = runProviderChecks(); + const cc = results.find(r => r.name === "claude-code"); + assert.ok(cc, "claude-code result should exist"); + assert.equal(cc!.status, "ok", "claude-code uses CLI auth — must be ok without API keys"); + assert.ok(cc!.message.includes("CLI auth"), "should indicate CLI auth"); + }); + }); + + rmSync(repo, { recursive: true, force: true }); + rmSync(tmpHome, { recursive: true, force: true }); +}); + test("PROVIDER_ROUTES includes google-gemini-cli as route for google (#2922)", async () => { const { readFileSync: readFS } = await import("node:fs"); const { dirname: dirn, join: joinPath } = await import("node:path");