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/__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}
, 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 && (