diff --git a/CHANGELOG.md b/CHANGELOG.md index de08c5bf11..e382e95747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ _In development — bullets added per PR; finalized at release._ ### 🔧 Bug Fixes +- **sse**: make the `anthropic-version` default-guard case-insensitive for `anthropic-compatible-*` providers, so a caller/operator-supplied `Anthropic-Version` (any casing) is no longer clobbered by a second lowercase `anthropic-version: 2023-06-01` header. (thanks @zakirkun) + - **huggingface**: validate API tokens via the `whoami-v2` endpoint as a pure auth probe so fine-grained Inference-Provider tokens (valid even when model/task endpoints reject them) are no longer falsely marked invalid; only 401/403 means an invalid key, other non-OK statuses surface as transient upstream errors. (thanks @Delcado19) - **sse/kiro**: reject the Anthropic-only `[1m]` context-1m suffix in `buildKiroPayload` before it reaches AWS Bedrock — Kiro is Bedrock-backed and cannot honor the beta, so a forwarded `kr/*[1m]` model id was malformed upstream; callers now get a clear error pointing them at a direct-Anthropic provider for 1M-context routing (thanks @Delcado19). diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 562f475ed1..b6f58a3ccc 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -491,7 +491,14 @@ export class DefaultExecutor extends BaseExecutor { headers["Authorization"] = `Bearer ${effectiveKey}`; } } - if (!headers["anthropic-version"]) { + // Default the anthropic-version header only when the caller/operator + // has not already supplied one. The lookup is case-insensitive so a + // pre-set "Anthropic-Version" (e.g. from this.config.headers or a + // custom header) is not clobbered with a duplicate lowercase entry. + const hasAnthropicVersion = Object.keys(headers).some( + (key) => key.toLowerCase() === "anthropic-version" + ); + if (!hasAnthropicVersion) { headers["anthropic-version"] = "2023-06-01"; } } else { diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index f4e7ed6e70..bdb6546c59 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -435,6 +435,43 @@ test("DefaultExecutor.buildHeaders handles GLM, default auth and anthropic-compa assert.equal(anthropicHeaders.Accept, "text/event-stream"); }); +test("DefaultExecutor.buildHeaders keeps a caller-supplied Anthropic-Version (case-insensitive guard) for anthropic-compatible providers", () => { + // An operator may configure a Title-Case "Anthropic-Version" via the provider + // config headers. The default-guard at the anthropic-compatible-* branch must + // detect it case-insensitively and NOT add a second lowercase + // "anthropic-version" key, which undici would otherwise combine into + // "2025-01-01, 2023-06-01" and break the upstream request. + const anthropicCompat = new DefaultExecutor("anthropic-compatible-test"); + // `config` is shared across instances via the provider registry, so snapshot + // and restore `config.headers` to avoid leaking the Title-Case override into + // other tests. + const originalConfigHeaders = anthropicCompat.config.headers; + anthropicCompat.config.headers = { + ...originalConfigHeaders, + "Anthropic-Version": "2025-01-01", + }; + + try { + const headers = anthropicCompat.buildHeaders({ apiKey: "anth-key" }, true); + + const versionKeys = Object.keys(headers).filter( + (key) => key.toLowerCase() === "anthropic-version" + ); + assert.equal(versionKeys.length, 1, "Duplicate anthropic-version header keys found"); + assert.equal(headers["Anthropic-Version"], "2025-01-01"); + assert.equal(headers["anthropic-version"], undefined); + assert.equal(headers["x-api-key"], "anth-key"); + } finally { + anthropicCompat.config.headers = originalConfigHeaders; + } +}); + +test("DefaultExecutor.buildHeaders still defaults anthropic-version when no variant is present", () => { + const anthropicCompat = new DefaultExecutor("anthropic-compatible-test"); + const headers = anthropicCompat.buildHeaders({ apiKey: "anth-key" }, true); + assert.equal(headers["anthropic-version"], "2023-06-01"); +}); + test("DefaultExecutor local OpenAI-style providers honor custom base URLs and skip empty bearer headers", () => { const lmStudio = new DefaultExecutor("lm-studio"); const vllm = new DefaultExecutor("vllm");