From 900d2fbd7c5e02918da4d2f99153e2ec4c66b1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Fri, 20 Mar 2026 09:56:29 -0600 Subject: [PATCH] fix(gsd): detect anthropic-vertex in provider doctor (#1598) * fix(gsd): detect anthropic-vertex in provider doctor * test(gsd): avoid secret-scan false positives --- .../extensions/gsd/doctor-providers.ts | 35 ++++++-- .../gsd/tests/doctor-providers.test.ts | 89 ++++++++++++++++++- 2 files changed, 112 insertions(+), 12 deletions(-) diff --git a/src/resources/extensions/gsd/doctor-providers.ts b/src/resources/extensions/gsd/doctor-providers.ts index 546a10f87..9bb2bf95f 100644 --- a/src/resources/extensions/gsd/doctor-providers.ts +++ b/src/resources/extensions/gsd/doctor-providers.ts @@ -51,10 +51,12 @@ function modelToProviderId(model: string): string | null { const prefix = model.split("/")[0].toLowerCase(); // Map known prefixes to registry IDs const prefixMap: Record = { + "anthropic-vertex": "anthropic-vertex", openrouter: "openrouter", groq: "groq", mistral: "mistral", google: "google", + "google-vertex": "google-vertex", anthropic: "anthropic", openai: "openai", "github-copilot": "github-copilot", @@ -88,11 +90,20 @@ function collectConfiguredModelProviders(): Set { const modelEntries = typeof models === "object" ? Object.values(models) : []; for (const entry of modelEntries) { - const modelId = typeof entry === "string" ? entry - : typeof entry === "object" && entry !== null && "model" in entry - ? String((entry as { model: unknown }).model) - : null; - if (modelId) { + if (typeof entry === "string") { + const pid = modelToProviderId(entry); + if (pid) providers.add(pid); + continue; + } + + if (typeof entry === "object" && entry !== null && "model" in entry) { + const configuredProvider = "provider" in entry ? (entry as { provider?: unknown }).provider : undefined; + if (typeof configuredProvider === "string" && configuredProvider.trim().length > 0) { + providers.add(configuredProvider); + continue; + } + + const modelId = String((entry as { model: unknown }).model); const pid = modelToProviderId(modelId); if (pid) providers.add(pid); } @@ -175,7 +186,9 @@ function checkLlmProviders(): ProviderCheckResult[] { for (const providerId of required) { const info = PROVIDER_REGISTRY.find(p => p.id === providerId); - const label = info?.label ?? providerId; + const label = providerId === "anthropic-vertex" + ? "Anthropic Vertex" + : info?.label ?? providerId; const lookup = resolveKey(providerId); if (!lookup.found) { @@ -196,14 +209,18 @@ function checkLlmProviders(): ProviderCheckResult[] { continue; } - const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`; + const envVar = providerId === "anthropic-vertex" + ? "ANTHROPIC_VERTEX_PROJECT_ID" + : info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`; results.push({ name: providerId, label, category: "llm", status: "error", - message: `${label} — no API key found`, - detail: info?.hasOAuth + message: `${label} — not configured`, + detail: providerId === "anthropic-vertex" + ? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC" + : info?.hasOAuth ? `Run /gsd keys to authenticate` : `Set ${envVar} or run /gsd keys`, required: true, diff --git a/src/resources/extensions/gsd/tests/doctor-providers.test.ts b/src/resources/extensions/gsd/tests/doctor-providers.test.ts index 404d50da1..c27d92e17 100644 --- a/src/resources/extensions/gsd/tests/doctor-providers.test.ts +++ b/src/resources/extensions/gsd/tests/doctor-providers.test.ts @@ -47,6 +47,18 @@ function withEnv(vars: Record, fn: () => void): void } } +function withCwd(nextCwd: string, fn: () => void): void { + const saved = process.cwd(); + process.chdir(nextCwd); + try { + fn(); + } finally { + process.chdir(saved); + } +} + +const PRESENT_TEST_VALUE = "configured"; + // ─── formatProviderReport ───────────────────────────────────────────────────── test("formatProviderReport returns fallback for empty results", () => { @@ -312,7 +324,7 @@ test("runProviderChecks reports ok for Anthropic when GitHub Copilot env var is withEnv({ ANTHROPIC_API_KEY: undefined, ANTHROPIC_OAUTH_TOKEN: undefined, - COPILOT_GITHUB_TOKEN: "ghu_copilot-token", + COPILOT_GITHUB_TOKEN: PRESENT_TEST_VALUE, GH_TOKEN: undefined, GITHUB_TOKEN: undefined, HOME: tmpHome, @@ -336,7 +348,7 @@ test("runProviderChecks reports ok for Anthropic via GITHUB_TOKEN cross-provider ANTHROPIC_OAUTH_TOKEN: undefined, COPILOT_GITHUB_TOKEN: undefined, GH_TOKEN: undefined, - GITHUB_TOKEN: "ghp_github-token", + GITHUB_TOKEN: PRESENT_TEST_VALUE, HOME: tmpHome, }, () => { try { @@ -354,7 +366,7 @@ test("runProviderChecks detects ANTHROPIC_OAUTH_TOKEN as valid Anthropic auth", const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-oauth-test-"))); withEnv({ ANTHROPIC_API_KEY: undefined, - ANTHROPIC_OAUTH_TOKEN: "oauth-token-test", + ANTHROPIC_OAUTH_TOKEN: PRESENT_TEST_VALUE, COPILOT_GITHUB_TOKEN: undefined, GH_TOKEN: undefined, GITHUB_TOKEN: undefined, @@ -401,3 +413,74 @@ test("runProviderChecks reports ok via Copilot auth.json for Anthropic", () => { rmSync(tmpHome, { recursive: true, force: true }); }); }); + +test("runProviderChecks uses provider-qualified anthropic-vertex model IDs", () => { + const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-home-"))); + const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-repo-"))); + mkdirSync(join(repo, ".gsd"), { recursive: true }); + writeFileSync( + join(repo, ".gsd", "preferences.md"), + [ + "---", + "models:", + " execution: anthropic-vertex/claude-sonnet-4-6", + "---", + "", + ].join("\n"), + ); + + withEnv({ + HOME: tmpHome, + ANTHROPIC_API_KEY: undefined, + ANTHROPIC_OAUTH_TOKEN: undefined, + ANTHROPIC_VERTEX_PROJECT_ID: "vertex-project", + }, () => { + withCwd(repo, () => { + const results = runProviderChecks(); + const vertex = results.find(r => r.name === "anthropic-vertex"); + const anthropic = results.find(r => r.name === "anthropic"); + assert.ok(vertex, "anthropic-vertex result should exist"); + assert.equal(vertex!.status, "ok", "should accept ANTHROPIC_VERTEX_PROJECT_ID as configured"); + assert.ok(!anthropic || !anthropic.required, "plain anthropic should not be required for anthropic-vertex config"); + }); + }); + + rmSync(repo, { recursive: true, force: true }); + rmSync(tmpHome, { recursive: true, force: true }); +}); + +test("runProviderChecks uses object provider field for anthropic-vertex models", () => { + const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-home-"))); + const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-repo-"))); + mkdirSync(join(repo, ".gsd"), { recursive: true }); + writeFileSync( + join(repo, ".gsd", "preferences.md"), + [ + "---", + "models:", + " execution:", + " model: claude-sonnet-4-6", + " provider: anthropic-vertex", + "---", + "", + ].join("\n"), + ); + + withEnv({ + HOME: tmpHome, + ANTHROPIC_API_KEY: undefined, + ANTHROPIC_OAUTH_TOKEN: undefined, + ANTHROPIC_VERTEX_PROJECT_ID: undefined, + }, () => { + withCwd(repo, () => { + const results = runProviderChecks(); + const vertex = results.find(r => r.name === "anthropic-vertex"); + assert.ok(vertex, "anthropic-vertex result should exist"); + assert.equal(vertex!.status, "error", "missing vertex config should be reported against anthropic-vertex"); + assert.ok(vertex!.detail?.includes("ANTHROPIC_VERTEX_PROJECT_ID"), "should point to vertex setup"); + }); + }); + + rmSync(repo, { recursive: true, force: true }); + rmSync(tmpHome, { recursive: true, force: true }); +});