This commit is contained in:
roomote-v0[bot] 2026-04-08 22:28:13 +00:00 committed by GitHub
commit 4e84f81d38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 120 additions and 2 deletions

View file

@ -207,6 +207,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

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

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" ||
@ -40,6 +47,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
@ -89,6 +140,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