Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/__tests__/models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { normalizedAnthropicBaseURL } from "../models";

describe("normalizedAnthropicBaseURL", () => {
const original = process.env.ANTHROPIC_BASE_URL;

beforeEach(() => {
delete process.env.ANTHROPIC_BASE_URL;
});

afterEach(() => {
if (original === undefined) {
delete process.env.ANTHROPIC_BASE_URL;
} else {
process.env.ANTHROPIC_BASE_URL = original;
}
});

it("returns undefined when ANTHROPIC_BASE_URL is unset", () => {
expect(normalizedAnthropicBaseURL()).toBeUndefined();
});

it("appends /v1 to a bare host (Claude Code / Cursor style)", () => {
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
expect(normalizedAnthropicBaseURL()).toBe("https://api.anthropic.com/v1");
});

it("trims a trailing slash before appending /v1", () => {
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com/";
expect(normalizedAnthropicBaseURL()).toBe("https://api.anthropic.com/v1");
});

it("leaves a value that already ends in /v1 unchanged", () => {
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
expect(normalizedAnthropicBaseURL()).toBe("https://api.anthropic.com/v1");
});

it("leaves a value that ends in /v1/ unchanged", () => {
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1/";
expect(normalizedAnthropicBaseURL()).toBe("https://api.anthropic.com/v1/");
});

it("normalizes a custom proxy host", () => {
process.env.ANTHROPIC_BASE_URL = "https://proxy.internal/anthropic";
expect(normalizedAnthropicBaseURL()).toBe("https://proxy.internal/anthropic/v1");
});
});
23 changes: 23 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ function getGoogleProvider() {
return _google;
}

/**
* Normalize a bare-host `ANTHROPIC_BASE_URL`.
*
* AI coding harnesses (Claude Code, Cursor) export `ANTHROPIC_BASE_URL` without
* the `/v1` suffix for their own runtime. `@ai-sdk/anthropic` prefers this env
* var over its `https://api.anthropic.com/v1` default and uses it verbatim, so
* a bare host drops `/v1` and every request 404s at `.../messages` — while
* other providers (e.g. Gemini) keep working, making consensus runs look
* half-broken. Append `/v1` when the configured base lacks a version path.
*
* Returns `undefined` when the env var is unset so the provider falls back to
* its own default. Remove once the SDK normalizes upstream.
*
* @see https://github.com/bug0inc/passmark/issues/57
* @see https://github.com/vercel/ai/issues/15542
*/
export function normalizedAnthropicBaseURL(): string | undefined {
const base = process.env.ANTHROPIC_BASE_URL;
if (!base) return undefined;
return /\/v1\/?$/.test(base) ? base : `${base.replace(/\/$/, "")}/v1`;
}

function getAnthropicProvider() {
if (!_anthropic) {
if (!process.env.ANTHROPIC_API_KEY) {
Expand All @@ -43,6 +65,7 @@ function getAnthropicProvider() {
}
_anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: normalizedAnthropicBaseURL(),
});
}
return _anthropic;
Expand Down