fix(gsd): detect anthropic-vertex in provider doctor (#1598)

* fix(gsd): detect anthropic-vertex in provider doctor

* test(gsd): avoid secret-scan false positives
This commit is contained in:
TÂCHES 2026-03-20 09:56:29 -06:00 committed by GitHub
parent 0bceb689a7
commit 900d2fbd7c
2 changed files with 112 additions and 12 deletions

View file

@ -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<string, string> = {
"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<string> {
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,

View file

@ -47,6 +47,18 @@ function withEnv(vars: Record<string, string | undefined>, 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 });
});