Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
23 changes: 23 additions & 0 deletions src/api/providers/__tests__/anthropic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
1 change: 1 addition & 0 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ vi.mock("@src/components/ui", () => ({
CollapsibleContent: ({ children }: any) => <div>{children}</div>,
Slider: ({ children, ...props }: any) => <div {...props}>{children}</div>,
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
// Add StandardTooltip for Anthropic custom headers UI
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
// Add Popover components for ModelPicker
Popover: ({ children }: any) => <div>{children}</div>,
PopoverTrigger: ({ children }: any) => <div>{children}</div>,
Expand Down
95 changes: 93 additions & 2 deletions webview-ui/src/components/settings/providers/Anthropic.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading