From a7e1f36bd7311d490fd62702abd82c101b878a13 Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Mon, 11 May 2026 07:53:22 -0700 Subject: [PATCH] anthropic workload identity federation --- .../proxy/src/providers/anthropic.test.ts | 21 +++++++++++++- packages/proxy/src/proxy.ts | 28 +++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/proxy/src/providers/anthropic.test.ts b/packages/proxy/src/providers/anthropic.test.ts index 132c1b18..ec628c2b 100644 --- a/packages/proxy/src/providers/anthropic.test.ts +++ b/packages/proxy/src/providers/anthropic.test.ts @@ -1,11 +1,12 @@ import { it, expect, describe } from "vitest"; import { callProxyV1, createCapturingFetch } from "../../utils/tests"; -import { FetchFn } from "../proxy"; +import { anthropicAuthHeaders, FetchFn } from "../proxy"; import { OpenAIChatCompletion, OpenAIChatCompletionChunk, OpenAIChatCompletionCreateParams, } from "@types"; +import { APISecretSchema } from "@schema"; import { omitUnsupportedAnthropicParams } from "./anthropic"; import { IMAGE_DATA_URL, @@ -72,6 +73,24 @@ it("should request identity encoding for streaming Anthropic chat completions", expect(requests[0].headers["accept-encoding"]).toBe("identity"); }); +it("should use bearer authorization for Anthropic WIF secrets", () => { + const headers = anthropicAuthHeaders( + APISecretSchema.parse({ + type: "anthropic", + secret: "test-wif-access-token", + name: "anthropic", + metadata: { + auth_type: "oauth_bearer", + auth_source: "anthropic_workload_identity_federation", + }, + }), + ); + + expect(headers).toEqual({ + authorization: "Bearer test-wif-access-token", + }); +}); + it("should mark streaming responses as no-transform", async () => { const encoder = new TextEncoder(); const anthropicEvents = [ diff --git a/packages/proxy/src/proxy.ts b/packages/proxy/src/proxy.ts index cd414cff..4eef5900 100644 --- a/packages/proxy/src/proxy.ts +++ b/packages/proxy/src/proxy.ts @@ -165,6 +165,30 @@ const GOOGLE_URL_REGEX = const GOOGLE_API_KEY_HEADER = "x-goog-api-key"; +function isAnthropicOAuthBearerSecret(secret: APISecret) { + return ( + secret.type === "anthropic" && + secret.metadata !== null && + secret.metadata !== undefined && + "auth_type" in secret.metadata && + secret.metadata.auth_type === "oauth_bearer" + ); +} + +export function anthropicAuthHeaders( + secret: APISecret, +): Record { + if (isAnthropicOAuthBearerSecret(secret)) { + return { + authorization: `Bearer ${secret.secret}`, + }; + } + + return { + "x-api-key": secret.secret, + }; +} + // Options to control how the cache key is generated. export interface CacheKeyOptions { excludeAuthToken?: boolean; @@ -2666,7 +2690,7 @@ async function fetchAnthropicMessages({ { method: "POST", headers: { - "x-api-key": secret.secret, + ...anthropicAuthHeaders(secret), "content-type": "application/json", "anthropic-version": "2023-06-01", }, @@ -2769,7 +2793,7 @@ async function fetchAnthropicChatCompletions({ headers["accept"] = "application/json"; headers["anthropic-version"] = "2023-06-01"; headers["host"] = fullURL.host; - headers["x-api-key"] = secret.secret; + Object.assign(headers, anthropicAuthHeaders(secret)); } if (isEmpty(bodyData)) {