mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(sse): dedupe case-variant Anthropic version/beta headers (#4846)
Integrated into release/v3.8.38 (leva 5)
This commit is contained in:
committed by
GitHub
parent
f8a17c4126
commit
f52863c80a
@@ -18,6 +18,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
- **fix(codebuddy):** only send reasoning params when the client requests reasoning. (thanks @anki1kr)
|
||||
- **fix(sse):** keep streaming for forceStream providers when a JSON client requests it. Providers marked `forceStream:true` reject `stream:false` upstream (HTTP 400); `resolveStreamFlag` now guards against this so stream-only providers keep streaming even when the client sends `Accept: application/json` or `stream:false`. (thanks @anki1kr)
|
||||
- **fix(sse):** prevent non-JSON SSE lines and duplicate `[DONE]` from breaking clients. (thanks @qianze0628)
|
||||
- **fix(sse):** dedupe case-variant Anthropic headers in the executor `buildHeaders` path — Node/undici's `fetch` merges `anthropic-version` and `Anthropic-Version` into a single `"v, v"` value that the Anthropic API rejects, so both case variants are now collapsed to one canonical lowercase header (same for `anthropic-beta`). (thanks @Delcado19)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -64,6 +64,49 @@ export function mergeClientAnthropicBeta(
|
||||
return baseList.join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse a list of comma-list header values into a deduped, trimmed token
|
||||
* array. Empty/undefined/null entries are dropped. Used to reconcile the
|
||||
* case-variant `anthropic-version` / `anthropic-beta` headers below.
|
||||
*/
|
||||
function uniqueCommaValues(values: Array<string | undefined | null>): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
values
|
||||
.filter((value) => value !== undefined && value !== null && value !== "")
|
||||
.flatMap((value) => String(value).split(","))
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedupe case-variant Anthropic headers in-place. Node/undici's fetch merges
|
||||
* `anthropic-version` and `Anthropic-Version` into a single `"v, v"` value,
|
||||
* which the Anthropic API rejects (#1475). Collapse both case variants down to
|
||||
* one canonical lowercase header carrying a single value. Same for
|
||||
* `anthropic-beta` (joined comma-list, deduped). Mutates `headers`.
|
||||
*/
|
||||
export function normalizeAnthropicHeaderVariants(headers: Record<string, string>): void {
|
||||
const versionValues = uniqueCommaValues([
|
||||
headers["anthropic-version"],
|
||||
headers["Anthropic-Version"],
|
||||
]);
|
||||
delete headers["Anthropic-Version"];
|
||||
delete headers["anthropic-version"];
|
||||
if (versionValues.length > 0) {
|
||||
headers["anthropic-version"] = versionValues[0];
|
||||
}
|
||||
|
||||
const betaValues = uniqueCommaValues([headers["anthropic-beta"], headers["Anthropic-Beta"]]);
|
||||
delete headers["Anthropic-Beta"];
|
||||
delete headers["anthropic-beta"];
|
||||
if (betaValues.length > 0) {
|
||||
headers["anthropic-beta"] = betaValues.join(",");
|
||||
}
|
||||
}
|
||||
|
||||
export const CLAUDE_CLI_VERSION = "2.1.187";
|
||||
export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`;
|
||||
export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.94.0";
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
|
||||
import { mergeClientAnthropicBeta } from "../config/anthropicHeaders.ts";
|
||||
import {
|
||||
mergeClientAnthropicBeta,
|
||||
normalizeAnthropicHeaderVariants,
|
||||
} from "../config/anthropicHeaders.ts";
|
||||
import { applyContextEditingToBody } from "../config/contextEditing.ts";
|
||||
import { findOffendingField, stripGroqUnsupportedFields } from "../config/providerFieldStrips.ts";
|
||||
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
|
||||
@@ -576,6 +579,8 @@ export class BaseExecutor {
|
||||
|
||||
headers["Accept"] = stream ? "text/event-stream" : "application/json";
|
||||
|
||||
normalizeAnthropicHeaderVariants(headers);
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
} from "../services/claudeCodeCompatible.ts";
|
||||
import { getGigachatAccessToken } from "../services/gigachatAuth.ts";
|
||||
import { getRegistryEntry } from "../config/providerRegistry.ts";
|
||||
import { mergeClientAnthropicBeta } from "../config/anthropicHeaders.ts";
|
||||
import {
|
||||
mergeClientAnthropicBeta,
|
||||
normalizeAnthropicHeaderVariants,
|
||||
} from "../config/anthropicHeaders.ts";
|
||||
import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
|
||||
import { stripUnsupportedParams } from "../translator/paramSupport.ts";
|
||||
import {
|
||||
@@ -573,6 +576,8 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
normalizeAnthropicHeaderVariants(headers);
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
67
tests/unit/anthropic-header-dedupe-1475.test.ts
Normal file
67
tests/unit/anthropic-header-dedupe-1475.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// Regression test for #1475 — duplicate case-variant Anthropic headers.
|
||||
//
|
||||
// Node/undici's fetch lowercases and MERGES same-name headers, so an outbound
|
||||
// set carrying both "anthropic-version" and "Anthropic-Version" collapses into
|
||||
// a single "anthropic-version: 2023-06-01, 2023-06-01" value, which the
|
||||
// Anthropic API rejects. normalizeAnthropicHeaderVariants() reconciles the two
|
||||
// case variants down to one canonical lowercase header with a single value
|
||||
// (and a deduped, joined comma-list for anthropic-beta) before the request is
|
||||
// dispatched in BaseExecutor.buildHeaders / DefaultExecutor.buildHeaders.
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { normalizeAnthropicHeaderVariants } from "../../open-sse/config/anthropicHeaders.ts";
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||||
|
||||
test("normalizeAnthropicHeaderVariants collapses case-variant anthropic-version into one value", () => {
|
||||
const headers: Record<string, string> = {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
};
|
||||
|
||||
normalizeAnthropicHeaderVariants(headers);
|
||||
|
||||
// The mixed-case variant must be gone, and the lowercase one must carry a
|
||||
// single value (no "v, v" duplication that Anthropic rejects).
|
||||
assert.equal(headers["Anthropic-Version"], undefined);
|
||||
assert.equal(headers["anthropic-version"], "2023-06-01");
|
||||
assert.ok(!headers["anthropic-version"].includes(","));
|
||||
});
|
||||
|
||||
test("normalizeAnthropicHeaderVariants dedupes and joins anthropic-beta variants", () => {
|
||||
const headers: Record<string, string> = {
|
||||
"anthropic-beta": "oauth-2025-04-20, prompt-caching",
|
||||
"Anthropic-Beta": "prompt-caching, fine-grained",
|
||||
};
|
||||
|
||||
normalizeAnthropicHeaderVariants(headers);
|
||||
|
||||
assert.equal(headers["Anthropic-Beta"], undefined);
|
||||
assert.equal(headers["anthropic-beta"], "oauth-2025-04-20,prompt-caching,fine-grained");
|
||||
});
|
||||
|
||||
test("normalizeAnthropicHeaderVariants leaves a single lowercase header untouched", () => {
|
||||
const headers: Record<string, string> = { "anthropic-version": "2023-06-01" };
|
||||
normalizeAnthropicHeaderVariants(headers);
|
||||
assert.equal(headers["anthropic-version"], "2023-06-01");
|
||||
assert.equal(headers["Anthropic-Version"], undefined);
|
||||
});
|
||||
|
||||
test("normalizeAnthropicHeaderVariants is a no-op when no anthropic headers are present", () => {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
normalizeAnthropicHeaderVariants(headers);
|
||||
assert.deepEqual(headers, { "Content-Type": "application/json" });
|
||||
});
|
||||
|
||||
test("DefaultExecutor.buildHeaders does not emit duplicate anthropic-version variants for an anthropic-compatible provider", () => {
|
||||
const executor = new DefaultExecutor("anthropic-compatible-test");
|
||||
const headers = executor.buildHeaders({ apiKey: "sk-test" }, true) as Record<string, string>;
|
||||
|
||||
// Whatever case the upstream registry/auth path used, only the canonical
|
||||
// lowercase header survives, with exactly one value.
|
||||
assert.equal(headers["Anthropic-Version"], undefined);
|
||||
if (headers["anthropic-version"] !== undefined) {
|
||||
assert.ok(!headers["anthropic-version"].includes(","));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user