mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
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:
parent
67e568f6bb
commit
49d5d38e32
4 changed files with 118 additions and 2 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue