diff --git a/packages/pi-ai/src/providers/anthropic-auth.test.ts b/packages/pi-ai/src/providers/anthropic-auth.test.ts new file mode 100644 index 000000000..4593e1a5d --- /dev/null +++ b/packages/pi-ai/src/providers/anthropic-auth.test.ts @@ -0,0 +1,32 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { usesAnthropicBearerAuth } from "./anthropic.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +test("usesAnthropicBearerAuth covers Bearer-only Anthropic-compatible providers (#3783)", () => { + assert.equal(usesAnthropicBearerAuth("alibaba-coding-plan"), true); + assert.equal(usesAnthropicBearerAuth("minimax"), true); + assert.equal(usesAnthropicBearerAuth("minimax-cn"), true); + assert.equal(usesAnthropicBearerAuth("anthropic"), false); +}); + +test("createClient routes Bearer-auth providers through authToken (#3783)", () => { + const source = readFileSync(join(__dirname, "..", "..", "src", "providers", "anthropic.ts"), "utf-8"); + assert.ok( + source.includes("const usesBearerAuth = usesAnthropicBearerAuth(model.provider);"), + "createClient should derive auth mode from usesAnthropicBearerAuth", + ); + assert.ok( + source.includes("apiKey: usesBearerAuth ? null : apiKey"), + "Bearer-auth providers should skip x-api-key auth", + ); + assert.ok( + source.includes("authToken: usesBearerAuth ? apiKey : undefined"), + "Bearer-auth providers should send authToken instead", + ); +}); diff --git a/packages/pi-ai/src/providers/anthropic.ts b/packages/pi-ai/src/providers/anthropic.ts index 57ee1b5be..ec9b21fde 100644 --- a/packages/pi-ai/src/providers/anthropic.ts +++ b/packages/pi-ai/src/providers/anthropic.ts @@ -44,6 +44,10 @@ function mergeHeaders(...headerSources: (Record | undefined)[]): return merged; } +export function usesAnthropicBearerAuth(provider: Model<"anthropic-messages">["provider"]): boolean { + return provider === "alibaba-coding-plan" || provider === "minimax" || provider === "minimax-cn"; +} + async function createClient( model: Model<"anthropic-messages">, apiKey: string, @@ -91,11 +95,11 @@ async function createClient( } // API key auth (Anthropic OAuth removed per TOS compliance — use API keys or Claude CLI) - // Alibaba Coding Plan uses Bearer token auth instead of x-api-key - const isAlibabaProvider = model.provider === "alibaba-coding-plan"; + // Some Anthropic-compatible providers require Bearer auth instead of x-api-key. + const usesBearerAuth = usesAnthropicBearerAuth(model.provider); const client = new AnthropicClass({ - apiKey: isAlibabaProvider ? null : apiKey, - authToken: isAlibabaProvider ? apiKey : undefined, + apiKey: usesBearerAuth ? null : apiKey, + authToken: usesBearerAuth ? apiKey : undefined, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders: mergeHeaders(