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 && (