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
This commit is contained in:
Roo Code 2026-01-29 15:14:45 +00:00
parent 67e568f6bb
commit 49d5d38e32
4 changed files with 118 additions and 2 deletions

View file

@ -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({

View file

@ -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", () => {

View file

@ -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,
})
}

View file

@ -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 (
<>
<VSCodeTextField
@ -86,6 +137,46 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
</>
)}
</div>
{/* Custom Headers UI */}
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<label className="block font-medium">{t("settings:providers.customHeaders")}</label>
<StandardTooltip content={t("settings:common.add")}>
<VSCodeButton appearance="icon" onClick={handleAddCustomHeader}>
<span className="codicon codicon-add"></span>
</VSCodeButton>
</StandardTooltip>
</div>
{!customHeaders.length ? (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.noCustomHeaders")}
</div>
) : (
customHeaders.map(([key, value], index) => (
<div key={index} className="flex items-center mb-2">
<VSCodeTextField
value={key}
className="flex-1 mr-2"
placeholder={t("settings:providers.headerName")}
onInput={(e: any) => handleUpdateHeaderKey(index, e.target.value)}
/>
<VSCodeTextField
value={value}
className="flex-1 mr-2"
placeholder={t("settings:providers.headerValue")}
onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)}
/>
<StandardTooltip content={t("settings:common.remove")}>
<VSCodeButton appearance="icon" onClick={() => handleRemoveCustomHeader(index)}>
<span className="codicon codicon-trash"></span>
</VSCodeButton>
</StandardTooltip>
</div>
))
)}
</div>
{supports1MContextBeta && (
<div>
<Checkbox