From 49d5d38e32d95777a9ba3aada6be636452577589 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 29 Jan 2026 15:14:45 +0000 Subject: [PATCH 1/2] feat(anthropic): add custom headers support for API gateways This change adds support for custom HTTP headers in the Anthropic provider, enabling users to pass headers required by API gateways like Portkey. Changes: - Add anthropicHeaders field to provider settings schema - Pass defaultHeaders to Anthropic SDK client constructor - Add custom headers UI in Anthropic settings (matching OpenAI pattern) - Add tests for custom headers functionality Closes #10939 --- packages/types/src/provider-settings.ts | 1 + src/api/providers/__tests__/anthropic.spec.ts | 23 +++++ src/api/providers/anthropic.ts | 1 + .../settings/providers/Anthropic.tsx | 95 ++++++++++++++++++- 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 0c5965f7ff6..2910e8210dd 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -193,6 +193,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({ anthropicBaseUrl: z.string().optional(), anthropicUseAuthToken: z.boolean().optional(), anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window. + anthropicHeaders: z.record(z.string(), z.string()).optional(), // Custom headers for Anthropic API (e.g., for API gateways like Portkey). }) const openRouterSchema = baseProviderSettingsSchema.extend({ diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 7a107edbc8b..03e7931a014 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -144,6 +144,29 @@ describe("AnthropicHandler", () => { expect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual("test-api-key") expect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined() }) + + it("should pass custom headers to the SDK when anthropicHeaders is provided", () => { + const customHeaders = { + "x-portkey-metadata": "test-metadata", + "x-custom-header": "custom-value", + } + const handlerWithHeaders = new AnthropicHandler({ + ...mockOptions, + anthropicHeaders: customHeaders, + }) + expect(handlerWithHeaders).toBeInstanceOf(AnthropicHandler) + expect(mockAnthropicConstructor).toHaveBeenCalledTimes(1) + expect(mockAnthropicConstructor.mock.calls[0]![0]!.defaultHeaders).toEqual(customHeaders) + }) + + it("should pass undefined defaultHeaders when anthropicHeaders is not provided", () => { + const handlerWithoutHeaders = new AnthropicHandler({ + ...mockOptions, + }) + expect(handlerWithoutHeaders).toBeInstanceOf(AnthropicHandler) + expect(mockAnthropicConstructor).toHaveBeenCalledTimes(1) + expect(mockAnthropicConstructor.mock.calls[0]![0]!.defaultHeaders).toBeUndefined() + }) }) describe("createMessage", () => { diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 3139f5d25a6..75d282de2ee 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -43,6 +43,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa this.client = new Anthropic({ baseURL: this.options.anthropicBaseUrl || undefined, [apiKeyFieldName]: this.options.apiKey, + defaultHeaders: this.options.anthropicHeaders || undefined, }) } diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index 46a239a3fd6..7634d762bad 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/webview-ui/src/components/settings/providers/Anthropic.tsx @@ -1,13 +1,15 @@ -import { useCallback, useState } from "react" +import { useCallback, useState, useEffect } from "react" import { Checkbox } from "vscrui" -import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" +import { StandardTooltip } from "@src/components/ui" +import { convertHeadersToObject } from "../utils/headers" import { inputEventTransform, noTransform } from "../transforms" type AnthropicProps = { @@ -22,6 +24,11 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) + const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { + const headers = apiConfiguration?.anthropicHeaders || {} + return Object.entries(headers) + }) + // Check if the current model supports 1M context beta const supports1MContextBeta = selectedModel?.id === "claude-sonnet-4-20250514" || selectedModel?.id === "claude-sonnet-4-5" @@ -37,6 +44,50 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro [setApiConfigurationField], ) + const handleAddCustomHeader = useCallback(() => { + // Only update the local state to show the new row in the UI. + setCustomHeaders((prev) => [...prev, ["", ""]]) + // Do not update the main configuration yet, wait for user input. + }, []) + + const handleUpdateHeaderKey = useCallback((index: number, newKey: string) => { + setCustomHeaders((prev) => { + const updated = [...prev] + + if (updated[index]) { + updated[index] = [newKey, updated[index][1]] + } + + return updated + }) + }, []) + + const handleUpdateHeaderValue = useCallback((index: number, newValue: string) => { + setCustomHeaders((prev) => { + const updated = [...prev] + + if (updated[index]) { + updated[index] = [updated[index][0], newValue] + } + + return updated + }) + }, []) + + const handleRemoveCustomHeader = useCallback((index: number) => { + setCustomHeaders((prev) => prev.filter((_, i) => i !== index)) + }, []) + + // Add effect to update the parent component's state when local headers change + useEffect(() => { + const timer = setTimeout(() => { + const headerObject = convertHeadersToObject(customHeaders) + setApiConfigurationField("anthropicHeaders", headerObject) + }, 300) + + return () => clearTimeout(timer) + }, [customHeaders, setApiConfigurationField]) + return ( <> )} + + {/* Custom Headers UI */} +
+
+ + + + + + +
+ {!customHeaders.length ? ( +
+ {t("settings:providers.noCustomHeaders")} +
+ ) : ( + customHeaders.map(([key, value], index) => ( +
+ handleUpdateHeaderKey(index, e.target.value)} + /> + handleUpdateHeaderValue(index, e.target.value)} + /> + + handleRemoveCustomHeader(index)}> + + + +
+ )) + )} +
+ {supports1MContextBeta && (
Date: Thu, 29 Jan 2026 15:35:14 +0000 Subject: [PATCH 2/2] fix: add StandardTooltip mock for Anthropic custom headers UI --- .../settings/__tests__/ApiOptions.provider-filtering.spec.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx index 544bd84a2a5..ed0084b1015 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx @@ -80,6 +80,8 @@ vi.mock("@src/components/ui", () => ({ CollapsibleContent: ({ children }: any) =>
{children}
, Slider: ({ children, ...props }: any) =>
{children}
, Button: ({ children, ...props }: any) => , + // Add StandardTooltip for Anthropic custom headers UI + StandardTooltip: ({ children, content }: any) =>
{children}
, // Add Popover components for ModelPicker Popover: ({ children }: any) =>
{children}
, PopoverTrigger: ({ children }: any) =>
{children}
,