fix(executors): anthropic-compatible-* gateways get Bearer alongside x-api-key (#4729)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 23:00:21 -03:00
committed by GitHub
parent b72a4d2fbb
commit ab7c181b77
2 changed files with 83 additions and 0 deletions

View File

@@ -469,6 +469,21 @@ export class DefaultExecutor extends BaseExecutor {
} else if (credentials.accessToken) {
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
}
// Port of decolua/9router commit b977bf74:
// Third-party Anthropic-compatible gateways frequently require
// Authorization: Bearer ALONGSIDE x-api-key — without it they
// return 401 missing_api_key on every forward. Only emit the
// Bearer fallback for non-official upstreams; api.anthropic.com
// (and the empty/default baseUrl that targets it) must keep the
// x-api-key-only behavior to avoid regressing the official path.
if (effectiveKey && !headers["Authorization"]) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || "";
const isOfficialAnthropic =
baseUrl === "" || baseUrl.includes("api.anthropic.com");
if (!isOfficialAnthropic) {
headers["Authorization"] = `Bearer ${effectiveKey}`;
}
}
if (!headers["anthropic-version"]) {
headers["anthropic-version"] = "2023-06-01";
}

View File

@@ -0,0 +1,68 @@
/**
* Port of decolua/9router commit b977bf74:
* Some third-party Anthropic-compatible gateways (configured via
* `anthropic-compatible-*` provider IDs) require Authorization: Bearer
* in addition to x-api-key. Without the Bearer header, those gateways
* return 401 "missing_api_key" on every forward.
*
* For NON-official anthropic-compatible endpoints (any `baseUrl` that is
* not empty AND does not contain "api.anthropic.com"), the default
* executor's buildHeaders must emit BOTH `x-api-key` and `Authorization:
* Bearer <apiKey>`. Official api.anthropic.com upstreams are unchanged
* (x-api-key only).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
const BASE_CREDS_THIRD_PARTY = {
apiKey: "k-third-party",
providerSpecificData: { baseUrl: "https://gateway.example/v1" },
} as Record<string, unknown>;
const BASE_CREDS_OFFICIAL = {
apiKey: "k-official",
providerSpecificData: { baseUrl: "https://api.anthropic.com/v1" },
} as Record<string, unknown>;
test("anthropic-compatible (third-party gateway): sends x-api-key AND Authorization: Bearer", () => {
const executor = new DefaultExecutor("anthropic-compatible-thirdparty");
const headers = executor.buildHeaders(BASE_CREDS_THIRD_PARTY, true) as Record<
string,
string
>;
assert.equal(headers["x-api-key"], "k-third-party");
assert.equal(
headers["Authorization"],
"Bearer k-third-party",
"third-party anthropic-compatible upstreams need the Bearer fallback too"
);
});
test("anthropic-compatible (official api.anthropic.com): only x-api-key, no Bearer", () => {
const executor = new DefaultExecutor("anthropic-compatible-official");
const headers = executor.buildHeaders(BASE_CREDS_OFFICIAL, true) as Record<
string,
string
>;
assert.equal(headers["x-api-key"], "k-official");
assert.equal(
headers["Authorization"],
undefined,
"official anthropic upstream must NOT receive a Bearer header alongside x-api-key"
);
});
test("anthropic-compatible (no baseUrl): treated as official, no Bearer", () => {
// Empty/missing baseUrl means: "talk to api.anthropic.com" — keep the legacy
// behavior (x-api-key only).
const executor = new DefaultExecutor("anthropic-compatible-empty");
const headers = executor.buildHeaders(
{ apiKey: "k-empty", providerSpecificData: {} } as Record<string, unknown>,
true
) as Record<string, string>;
assert.equal(headers["x-api-key"], "k-empty");
assert.equal(headers["Authorization"], undefined);
});