Merge PR #5890: fix(ai): allow custom OAuth fingerprint header overrides (@roboomp)
This commit is contained in:
@@ -35,6 +35,10 @@
|
||||
- Automatically invalidate and rotate OAuth credentials when an "invalidated oauth token" error occurs
|
||||
- Fixed Anthropic usage reports treating the organization response header as the account identity, which caused the 5h/7d status-line segment to disappear for OAuth credentials without stored organization metadata. ([#5698](https://github.com/can1357/oh-my-pi/issues/5698))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed custom OAuth Anthropic-compatible endpoints with explicit header overrides still receiving generated Claude Code fingerprint headers instead. ([#5888](https://github.com/can1357/oh-my-pi/issues/5888))
|
||||
|
||||
## [17.0.2] - 2026-07-17
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -100,6 +100,8 @@ export type AnthropicHeaderOptions = {
|
||||
isCloudflareAiGateway?: boolean;
|
||||
claudeCodeSessionId?: string;
|
||||
claudeCodeBetas?: readonly string[];
|
||||
/** Allow explicit fingerprint headers to replace OAuth defaults on non-official endpoints. */
|
||||
allowAnthropicHeaderOverrides?: boolean;
|
||||
};
|
||||
|
||||
export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
|
||||
@@ -226,22 +228,36 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
||||
const acceptHeader = oauthToken ? "application/json" : stream ? "text/event-stream" : "application/json";
|
||||
const isCloudflare = options.isCloudflareAiGateway ?? false;
|
||||
const honorAuthorization = !oauthToken && !isCloudflare;
|
||||
const allowAnthropicHeaderOverrides =
|
||||
oauthToken &&
|
||||
options.allowAnthropicHeaderOverrides === true &&
|
||||
!isCloudflare &&
|
||||
!isOfficialAnthropicApiUrl(options.baseUrl);
|
||||
const honorApiKey = !isCloudflare;
|
||||
const modelHeaders: Record<string, string> = {};
|
||||
const anthropicHeaderOverrides: Record<string, string> = {};
|
||||
const filteredEnforcedKeys: string[] = [];
|
||||
for (const [key, value] of Object.entries(options.modelHeaders ?? {})) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (enforcedHeaderKeys.has(lowerKey)) {
|
||||
// user-agent is always re-applied explicitly. authorization / x-api-key
|
||||
// are silently re-applied in honoring branches and dropped + logged
|
||||
// where the branch enforces its own credential.
|
||||
if (lowerKey === "user-agent") continue;
|
||||
if (lowerKey === "authorization" && honorAuthorization) continue;
|
||||
if (lowerKey === "x-api-key" && honorApiKey) continue;
|
||||
filteredEnforcedKeys.push(key);
|
||||
continue;
|
||||
const headerSource = options.modelHeaders;
|
||||
if (headerSource) {
|
||||
for (const key in headerSource) {
|
||||
const value = headerSource[key];
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (enforcedHeaderKeys.has(lowerKey)) {
|
||||
if (allowAnthropicHeaderOverrides && overridableAnthropicHeaderKeys.has(lowerKey)) {
|
||||
anthropicHeaderOverrides[key] = value;
|
||||
continue;
|
||||
}
|
||||
// user-agent is always re-applied explicitly. authorization / x-api-key
|
||||
// are silently re-applied in honoring branches and dropped + logged
|
||||
// where the branch enforces its own credential.
|
||||
if (lowerKey === "user-agent") continue;
|
||||
if (lowerKey === "authorization" && honorAuthorization) continue;
|
||||
if (lowerKey === "x-api-key" && honorApiKey) continue;
|
||||
filteredEnforcedKeys.push(key);
|
||||
continue;
|
||||
}
|
||||
modelHeaders[key] = value;
|
||||
}
|
||||
modelHeaders[key] = value;
|
||||
}
|
||||
if (filteredEnforcedKeys.length > 0) {
|
||||
// Caller/env-supplied values (options.headers, ANTHROPIC_CUSTOM_HEADERS)
|
||||
@@ -267,7 +283,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
||||
const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
|
||||
? incomingUserAgent
|
||||
: `claude-cli/${claudeCodeVersion} (external, local-agent, agent-sdk/${claudeAgentSdkVersion})`;
|
||||
return {
|
||||
const headers = {
|
||||
...modelHeaders,
|
||||
...claudeCodeHeaders,
|
||||
Accept: acceptHeader,
|
||||
@@ -279,6 +295,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
||||
"User-Agent": userAgent,
|
||||
...(incomingApiKey ? { "X-Api-Key": incomingApiKey } : {}),
|
||||
};
|
||||
return allowAnthropicHeaderOverrides ? mergeHeaders(headers, anthropicHeaderOverrides) : headers;
|
||||
} else if (!isOfficialAnthropicApiUrl(options.baseUrl)) {
|
||||
return {
|
||||
...modelHeaders,
|
||||
@@ -521,6 +538,10 @@ const enforcedHeaderKeys = new Set(
|
||||
].map(key => key.toLowerCase()),
|
||||
);
|
||||
|
||||
const overridableAnthropicHeaderKeys = new Set(
|
||||
[...Object.keys(claudeCodeHeaders), "anthropic-beta", "User-Agent", "x-app"].map(key => key.toLowerCase()),
|
||||
);
|
||||
|
||||
const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
||||
|
||||
function createClaudeBillingHeader(firstUserMessageText: string): string {
|
||||
@@ -2811,6 +2832,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
||||
dynamicHeaders,
|
||||
),
|
||||
isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
|
||||
allowAnthropicHeaderOverrides: model.compat.allowAnthropicHeaderOverrides,
|
||||
claudeCodeSessionId,
|
||||
claudeCodeBetas: oauthToken
|
||||
? buildClaudeCodeBetas(
|
||||
|
||||
@@ -677,6 +677,48 @@ describe("Anthropic request fingerprint alignment", () => {
|
||||
expect(headers.Authorization).toBe("Bearer sk-ant-oat-test");
|
||||
});
|
||||
|
||||
it("honors opted-in OAuth fingerprint headers on non-official endpoints (#5888)", () => {
|
||||
const options = buildAnthropicClientOptions({
|
||||
model: buildModel({
|
||||
...ANTHROPIC_MODEL_SPEC,
|
||||
provider: "custom-anthropic",
|
||||
baseUrl: "https://proxy.example.com/anthropic",
|
||||
headers: {
|
||||
"anthropic-beta": "custom-beta-token",
|
||||
"x-app": "custom-app-token",
|
||||
"X-Stainless-Runtime-Version": "custom-runtime-token",
|
||||
Authorization: "should-not-leak",
|
||||
},
|
||||
compat: { allowAnthropicHeaderOverrides: true },
|
||||
}),
|
||||
apiKey: "sk-ant-oat-test",
|
||||
stream: true,
|
||||
});
|
||||
|
||||
expect(options.defaultHeaders["anthropic-beta"]).toBe("custom-beta-token");
|
||||
expect(options.defaultHeaders["x-app"]).toBe("custom-app-token");
|
||||
expect(options.defaultHeaders["X-Stainless-Runtime-Version"]).toBe("custom-runtime-token");
|
||||
expect(options.defaultHeaders.Authorization).toBe("Bearer sk-ant-oat-test");
|
||||
});
|
||||
|
||||
it("keeps OAuth fingerprint defaults on official endpoints despite the compat opt-in", () => {
|
||||
const headers = buildAnthropicHeaders({
|
||||
apiKey: "sk-ant-oat-test",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
isOAuth: true,
|
||||
allowAnthropicHeaderOverrides: true,
|
||||
modelHeaders: {
|
||||
"anthropic-beta": "custom-beta-token",
|
||||
"x-app": "custom-app-token",
|
||||
"X-Stainless-Runtime-Version": "custom-runtime-token",
|
||||
},
|
||||
});
|
||||
|
||||
expect(headers["anthropic-beta"]).not.toBe("custom-beta-token");
|
||||
expect(headers["x-app"]).toBe("cli");
|
||||
expect(headers["X-Stainless-Runtime-Version"]).toBe("v24.3.0");
|
||||
});
|
||||
|
||||
it("suppresses the client-level X-Api-Key when model.headers carries a custom Authorization (#3391)", () => {
|
||||
const options = buildAnthropicClientOptions({
|
||||
model: buildModel({
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
- Logged LiteLLM rich-metadata endpoint failures once with their endpoint and status before falling back to incomplete `/v1/models` data ([#5801](https://github.com/can1357/oh-my-pi/issues/5801)).
|
||||
- Fixed authenticated Kimi Code discovery to preserve live effort levels, default effort, mandatory-thinking state, and per-model protocol metadata ([#5893](https://github.com/can1357/oh-my-pi/issues/5893)).
|
||||
- Fixed LiteLLM provider ignoring per-model pricing: `mapLiteLLMRichEntry` now reads `input_cost_per_token` / `output_cost_per_token` (plus cache costs) from LiteLLM rich metadata and maps them to `cost.input` / `cost.output`, falling back to the bundled reference only when LiteLLM omits cost, so proxied models no longer display as free ([#5818](https://github.com/can1357/oh-my-pi/issues/5818)).
|
||||
### Added
|
||||
|
||||
- Added an Anthropic compatibility flag for opting non-official OAuth endpoints into configured Claude Code fingerprint header overrides. ([#5888](https://github.com/can1357/oh-my-pi/issues/5888))
|
||||
|
||||
## [17.0.2] - 2026-07-17
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res
|
||||
signingEndpoint,
|
||||
disableStrictTools: isAzure,
|
||||
disableAdaptiveThinking: false,
|
||||
allowAnthropicHeaderOverrides: false,
|
||||
supportsEagerToolInputStreaming: official,
|
||||
// Long cache retention is only sent to the official API by default;
|
||||
// proxies opt in explicitly via `compat.supportsLongCacheRetention: true`.
|
||||
|
||||
@@ -418,6 +418,11 @@ export interface AnthropicCompat {
|
||||
* auto-detected (Z.AI hosts).
|
||||
*/
|
||||
requiresToolResultId?: boolean;
|
||||
/**
|
||||
* Allow configured Claude Code fingerprint headers to replace generated
|
||||
* OAuth defaults on non-official Anthropic endpoints.
|
||||
*/
|
||||
allowAnthropicHeaderOverrides?: boolean;
|
||||
/**
|
||||
* Replay unsigned `thinking` blocks from prior assistant turns as native
|
||||
* thinking instead of demoting them to text. Official Anthropic enforces
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
|
||||
- Removed the unreliable Bing and Yahoo HTML-scraping web search providers
|
||||
- Fixed the Cursor-backed advisor losing entire turns when it selected server-native tools (`bash`, `grep`, etc.) outside its grant: exec-resolved native blocks are already rejected in-band by the advisor-scoped bridge, so they no longer trip the unavailable-tool quarantine and discard the `advise` emitted in the same turn ([#5900](https://github.com/can1357/oh-my-pi/issues/5900)).
|
||||
- Fixed custom `anthropic-messages` OAuth providers being unable to opt into configured Claude Code fingerprint header overrides. ([#5888](https://github.com/can1357/oh-my-pi/issues/5888))
|
||||
|
||||
## [17.0.2] - 2026-07-17
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ const OpenAICompatFields = {
|
||||
"supportsImageDetailOriginal?": "boolean",
|
||||
// anthropic-messages compat flags (same `compat` slot, per-api interpretation)
|
||||
"supportsEagerToolInputStreaming?": "boolean",
|
||||
"allowAnthropicHeaderOverrides?": "boolean",
|
||||
"requiresToolResultId?": "boolean",
|
||||
"replayUnsignedThinking?": "boolean",
|
||||
} as const;
|
||||
|
||||
@@ -584,6 +584,7 @@ describe("ModelRegistry", () => {
|
||||
api: "anthropic-messages",
|
||||
compat: {
|
||||
supportsEagerToolInputStreaming: true,
|
||||
allowAnthropicHeaderOverrides: true,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
@@ -685,7 +686,10 @@ describe("ModelRegistry", () => {
|
||||
|
||||
test("custom Anthropic providers can opt into eager tool input streaming", () => {
|
||||
const model = customAnthropicCompat.find("anthropic-proxy", "claude-haiku-4.5");
|
||||
expect(model?.compat).toMatchObject({ supportsEagerToolInputStreaming: true });
|
||||
expect(model?.compat).toMatchObject({
|
||||
supportsEagerToolInputStreaming: true,
|
||||
allowAnthropicHeaderOverrides: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("custom Responses providers can disable original image detail", () => {
|
||||
|
||||
Reference in New Issue
Block a user