Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 49d5d38

Browse files
committed
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
1 parent 67e568f commit 49d5d38

4 files changed

Lines changed: 118 additions & 2 deletions

File tree

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
193193
anthropicBaseUrl: z.string().optional(),
194194
anthropicUseAuthToken: z.boolean().optional(),
195195
anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
196+
anthropicHeaders: z.record(z.string(), z.string()).optional(), // Custom headers for Anthropic API (e.g., for API gateways like Portkey).
196197
})
197198

198199
const openRouterSchema = baseProviderSettingsSchema.extend({

src/api/providers/__tests__/anthropic.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,29 @@ describe("AnthropicHandler", () => {
144144
expect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual("test-api-key")
145145
expect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()
146146
})
147+
148+
it("should pass custom headers to the SDK when anthropicHeaders is provided", () => {
149+
const customHeaders = {
150+
"x-portkey-metadata": "test-metadata",
151+
"x-custom-header": "custom-value",
152+
}
153+
const handlerWithHeaders = new AnthropicHandler({
154+
...mockOptions,
155+
anthropicHeaders: customHeaders,
156+
})
157+
expect(handlerWithHeaders).toBeInstanceOf(AnthropicHandler)
158+
expect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)
159+
expect(mockAnthropicConstructor.mock.calls[0]![0]!.defaultHeaders).toEqual(customHeaders)
160+
})
161+
162+
it("should pass undefined defaultHeaders when anthropicHeaders is not provided", () => {
163+
const handlerWithoutHeaders = new AnthropicHandler({
164+
...mockOptions,
165+
})
166+
expect(handlerWithoutHeaders).toBeInstanceOf(AnthropicHandler)
167+
expect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)
168+
expect(mockAnthropicConstructor.mock.calls[0]![0]!.defaultHeaders).toBeUndefined()
169+
})
147170
})
148171

149172
describe("createMessage", () => {

src/api/providers/anthropic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
4343
this.client = new Anthropic({
4444
baseURL: this.options.anthropicBaseUrl || undefined,
4545
[apiKeyFieldName]: this.options.apiKey,
46+
defaultHeaders: this.options.anthropicHeaders || undefined,
4647
})
4748
}
4849

webview-ui/src/components/settings/providers/Anthropic.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { useCallback, useState } from "react"
1+
import { useCallback, useState, useEffect } from "react"
22
import { Checkbox } from "vscrui"
3-
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3+
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
44

55
import type { ProviderSettings } from "@roo-code/types"
66

77
import { useAppTranslation } from "@src/i18n/TranslationContext"
88
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
99
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
10+
import { StandardTooltip } from "@src/components/ui"
1011

12+
import { convertHeadersToObject } from "../utils/headers"
1113
import { inputEventTransform, noTransform } from "../transforms"
1214

1315
type AnthropicProps = {
@@ -22,6 +24,11 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
2224

2325
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
2426

27+
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
28+
const headers = apiConfiguration?.anthropicHeaders || {}
29+
return Object.entries(headers)
30+
})
31+
2532
// Check if the current model supports 1M context beta
2633
const supports1MContextBeta =
2734
selectedModel?.id === "claude-sonnet-4-20250514" || selectedModel?.id === "claude-sonnet-4-5"
@@ -37,6 +44,50 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
3744
[setApiConfigurationField],
3845
)
3946

47+
const handleAddCustomHeader = useCallback(() => {
48+
// Only update the local state to show the new row in the UI.
49+
setCustomHeaders((prev) => [...prev, ["", ""]])
50+
// Do not update the main configuration yet, wait for user input.
51+
}, [])
52+
53+
const handleUpdateHeaderKey = useCallback((index: number, newKey: string) => {
54+
setCustomHeaders((prev) => {
55+
const updated = [...prev]
56+
57+
if (updated[index]) {
58+
updated[index] = [newKey, updated[index][1]]
59+
}
60+
61+
return updated
62+
})
63+
}, [])
64+
65+
const handleUpdateHeaderValue = useCallback((index: number, newValue: string) => {
66+
setCustomHeaders((prev) => {
67+
const updated = [...prev]
68+
69+
if (updated[index]) {
70+
updated[index] = [updated[index][0], newValue]
71+
}
72+
73+
return updated
74+
})
75+
}, [])
76+
77+
const handleRemoveCustomHeader = useCallback((index: number) => {
78+
setCustomHeaders((prev) => prev.filter((_, i) => i !== index))
79+
}, [])
80+
81+
// Add effect to update the parent component's state when local headers change
82+
useEffect(() => {
83+
const timer = setTimeout(() => {
84+
const headerObject = convertHeadersToObject(customHeaders)
85+
setApiConfigurationField("anthropicHeaders", headerObject)
86+
}, 300)
87+
88+
return () => clearTimeout(timer)
89+
}, [customHeaders, setApiConfigurationField])
90+
4091
return (
4192
<>
4293
<VSCodeTextField
@@ -86,6 +137,46 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
86137
</>
87138
)}
88139
</div>
140+
141+
{/* Custom Headers UI */}
142+
<div className="mb-4">
143+
<div className="flex justify-between items-center mb-2">
144+
<label className="block font-medium">{t("settings:providers.customHeaders")}</label>
145+
<StandardTooltip content={t("settings:common.add")}>
146+
<VSCodeButton appearance="icon" onClick={handleAddCustomHeader}>
147+
<span className="codicon codicon-add"></span>
148+
</VSCodeButton>
149+
</StandardTooltip>
150+
</div>
151+
{!customHeaders.length ? (
152+
<div className="text-sm text-vscode-descriptionForeground">
153+
{t("settings:providers.noCustomHeaders")}
154+
</div>
155+
) : (
156+
customHeaders.map(([key, value], index) => (
157+
<div key={index} className="flex items-center mb-2">
158+
<VSCodeTextField
159+
value={key}
160+
className="flex-1 mr-2"
161+
placeholder={t("settings:providers.headerName")}
162+
onInput={(e: any) => handleUpdateHeaderKey(index, e.target.value)}
163+
/>
164+
<VSCodeTextField
165+
value={value}
166+
className="flex-1 mr-2"
167+
placeholder={t("settings:providers.headerValue")}
168+
onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)}
169+
/>
170+
<StandardTooltip content={t("settings:common.remove")}>
171+
<VSCodeButton appearance="icon" onClick={() => handleRemoveCustomHeader(index)}>
172+
<span className="codicon codicon-trash"></span>
173+
</VSCodeButton>
174+
</StandardTooltip>
175+
</div>
176+
))
177+
)}
178+
</div>
179+
89180
{supports1MContextBeta && (
90181
<div>
91182
<Checkbox

0 commit comments

Comments
 (0)