From 90abe0304a2bb0a447e921e5fece1e8fd3467faf Mon Sep 17 00:00:00 2001 From: AlgerMusic Build Bot Date: Sun, 28 Jun 2026 15:58:42 +0800 Subject: [PATCH 1/2] Add Anthropic Custom provider --- packages/types/src/provider-settings.ts | 18 +- src/api/index.ts | 1 + src/api/providers/anthropic.ts | 23 +- src/shared/ProfileValidator.ts | 2 + .../src/components/settings/ApiOptions.tsx | 11 + .../src/components/settings/ModelPicker.tsx | 1 + .../src/components/settings/constants.ts | 1 + .../settings/providers/AnthropicCustom.tsx | 487 ++++++++++++++++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 11 +- webview-ui/src/utils/validate.ts | 13 + 11 files changed, 558 insertions(+), 11 deletions(-) create mode 100644 webview-ui/src/components/settings/providers/AnthropicCustom.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 26c4dee7e1..800e40f6e9 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -85,7 +85,7 @@ export const isInternalProvider = (key: string): key is InternalProvider => * Custom providers are completely configurable within Roo Code settings. */ -export const customProviders = ["openai"] as const +export const customProviders = ["openai", "anthropic-custom"] as const export type CustomProvider = (typeof customProviders)[number] @@ -262,6 +262,15 @@ const openAiSchema = baseProviderSettingsSchema.extend({ openAiHeaders: z.record(z.string(), z.string()).optional(), }) +const anthropicCustomSchema = baseProviderSettingsSchema.extend({ + anthropicCustomBaseUrl: z.string().optional(), + anthropicCustomApiKey: z.string().optional(), + anthropicCustomModelId: z.string().optional(), + anthropicCustomModelInfo: modelInfoSchema.nullish(), + anthropicCustomStreamingEnabled: z.boolean().optional(), + anthropicCustomHeaders: z.record(z.string(), z.string()).optional(), +}) + const ollamaSchema = baseProviderSettingsSchema.extend({ ollamaModelId: z.string().optional(), ollamaBaseUrl: z.string().optional(), @@ -427,6 +436,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })), vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })), openAiSchema.merge(z.object({ apiProvider: z.literal("openai") })), + anthropicCustomSchema.merge(z.object({ apiProvider: z.literal("anthropic-custom") })), ollamaSchema.merge(z.object({ apiProvider: z.literal("ollama") })), vsCodeLmSchema.merge(z.object({ apiProvider: z.literal("vscode-lm") })), lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })), @@ -463,6 +473,7 @@ export const providerSettingsSchema = z.object({ ...bedrockSchema.shape, ...vertexSchema.shape, ...openAiSchema.shape, + ...anthropicCustomSchema.shape, ...ollamaSchema.shape, ...vsCodeLmSchema.shape, ...lmStudioSchema.shape, @@ -512,6 +523,7 @@ export const modelIdKeys = [ "apiModelId", "openRouterModelId", "openAiModelId", + "anthropicCustomModelId", "ollamaModelId", "lmStudioModelId", "lmStudioDraftModelId", @@ -575,7 +587,7 @@ export const modelIdKeysByProvider: Record = { */ // Providers that use Anthropic-style API protocol. -export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"] +export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "anthropic-custom", "bedrock", "minimax"] export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => { if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) { @@ -615,7 +627,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str */ export const MODELS_BY_PROVIDER: Record< - Exclude, + Exclude, { id: ProviderName; label: string; models: string[] } > = { anthropic: { diff --git a/src/api/index.ts b/src/api/index.ts index 0c901f8e23..4618b7b6ba 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -133,6 +133,7 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { switch (apiProvider) { case "anthropic": + case "anthropic-custom": return new AnthropicHandler(options) case "openrouter": return new OpenRouterHandler(options) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 7a4ef30ad0..9a22e80b92 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -8,6 +8,7 @@ import { type AnthropicModelId, anthropicDefaultModelId, anthropicModels, + openAiModelInfoSaneDefaults, ANTHROPIC_DEFAULT_MAX_TOKENS, ApiProviderError, } from "@roo-code/types" @@ -38,12 +39,17 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa super() this.options = options + const baseURL = this.options.anthropicCustomBaseUrl || this.options.anthropicBaseUrl || undefined + const apiKey = this.options.anthropicCustomApiKey || this.options.apiKey const apiKeyFieldName = - this.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? "authToken" : "apiKey" + baseURL && this.options.anthropicUseAuthToken && !this.options.anthropicCustomApiKey + ? "authToken" + : "apiKey" this.client = new Anthropic({ - baseURL: this.options.anthropicBaseUrl || undefined, - [apiKeyFieldName]: this.options.apiKey, + baseURL, + [apiKeyFieldName]: apiKey, + defaultHeaders: this.options.anthropicCustomHeaders, timeout: this.timeoutMs, }) } @@ -352,9 +358,14 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa } getModel() { - const modelId = this.options.apiModelId - const id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId - let info: ModelInfo = anthropicModels[id] + const customModelId = this.options.anthropicCustomModelId + const modelId = customModelId || this.options.apiModelId + const id = + customModelId || + (modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId) + let info: ModelInfo = customModelId + ? this.options.anthropicCustomModelInfo || openAiModelInfoSaneDefaults + : anthropicModels[id as AnthropicModelId] // If 1M context beta is enabled for supported models, update the model info if ( diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 7246a90177..fd1703c72c 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -53,6 +53,8 @@ export class ProfileValidator { switch (profile.apiProvider) { case "openai": return profile.openAiModelId + case "anthropic-custom": + return profile.anthropicCustomModelId case "anthropic": case "openai-native": case "bedrock": diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 70617a1ee6..db9ee93da2 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -48,6 +48,7 @@ import { import { Anthropic, + AnthropicCustom, Baseten, Bedrock, DeepSeek, @@ -468,6 +469,16 @@ const ApiOptions = ({ /> )} + {selectedProvider === "anthropic-custom" && ( + + )} + {selectedProvider === "openai-codex" && ( ( + field: K, + value: ProviderSettings[K], + isUserAction?: boolean, + ) => void + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const AnthropicCustom = ({ + apiConfiguration, + setApiConfigurationField, + organizationAllowList, + modelValidationError, + simplifySettings, +}: AnthropicCustomProps) => { + const { t } = useAppTranslation() + + const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { + const headers = apiConfiguration?.anthropicCustomHeaders || {} + return Object.entries(headers) + }) + + const handleAddCustomHeader = useCallback(() => { + setCustomHeaders((prev) => [...prev, ["", ""]]) + }, []) + + 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)) + }, []) + + useEffect(() => { + const timer = setTimeout(() => { + const headerObject = convertHeadersToObject(customHeaders) + setApiConfigurationField("anthropicCustomHeaders", headerObject, false) + }, 300) + + return () => clearTimeout(timer) + }, [customHeaders, setApiConfigurationField]) + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + + + + + + + {t("settings:modelInfo.enableStreaming")} + + + {/* Custom Headers UI */} +
+
+ + + + + + +
+ {!customHeaders.length ? ( +
+ {t("settings:providers.noCustomHeaders")} +
+ ) : ( + customHeaders.map(([key, value], index) => ( +
+ handleUpdateHeaderKey(index, e.target.value)} + /> + handleUpdateHeaderValue(index, e.target.value)} + /> + + handleRemoveCustomHeader(index)}> + + + +
+ )) + )} +
+ +
+
+ {t("settings:providers.customModel.capabilities")} +
+ +
+ { + const value = apiConfiguration?.anthropicCustomModelInfo?.maxTokens + + if (!value) { + return "var(--vscode-input-border)" + } + + return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" + })(), + }} + onInput={handleInputChange("anthropicCustomModelInfo", (e) => { + const value = parseInt((e.target as HTMLInputElement).value) + + return { + ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), + maxTokens: isNaN(value) ? undefined : value, + } + })} + placeholder={t("settings:placeholders.numbers.maxTokens")} + className="w-full"> + + +
+ {t("settings:providers.customModel.maxTokens.description")} +
+
+ +
+ { + const value = apiConfiguration?.anthropicCustomModelInfo?.contextWindow + + if (!value) { + return "var(--vscode-input-border)" + } + + return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" + })(), + }} + onInput={handleInputChange("anthropicCustomModelInfo", (e) => { + const value = (e.target as HTMLInputElement).value + const parsed = parseInt(value) + + return { + ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), + contextWindow: isNaN(parsed) ? openAiModelInfoSaneDefaults.contextWindow : parsed, + } + })} + placeholder={t("settings:placeholders.numbers.contextWindow")} + className="w-full"> + + +
+ {t("settings:providers.customModel.contextWindow.description")} +
+
+ +
+
+ { + return { + ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), + supportsImages: checked, + } + })}> + + {t("settings:providers.customModel.imageSupport.label")} + + + + + +
+
+ {t("settings:providers.customModel.imageSupport.description")} +
+
+ +
+
+ { + return { + ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), + supportsPromptCache: checked, + } + })}> + {t("settings:providers.customModel.promptCache.label")} + + + + +
+
+ {t("settings:providers.customModel.promptCache.description")} +
+
+ +
+ { + const value = apiConfiguration?.anthropicCustomModelInfo?.inputPrice + + if (!value && value !== 0) { + return "var(--vscode-input-border)" + } + + return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" + })(), + }} + onChange={handleInputChange("anthropicCustomModelInfo", (e) => { + const value = (e.target as HTMLInputElement).value + const parsed = parseFloat(value) + + return { + ...(apiConfiguration?.anthropicCustomModelInfo ?? openAiModelInfoSaneDefaults), + inputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed, + } + })} + placeholder={t("settings:placeholders.numbers.inputPrice")} + className="w-full"> +
+ + + + +
+
+
+ +
+ { + const value = apiConfiguration?.anthropicCustomModelInfo?.outputPrice + + if (!value && value !== 0) { + return "var(--vscode-input-border)" + } + + return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" + })(), + }} + onChange={handleInputChange("anthropicCustomModelInfo", (e) => { + const value = (e.target as HTMLInputElement).value + const parsed = parseFloat(value) + + return { + ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), + outputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed, + } + })} + placeholder={t("settings:placeholders.numbers.outputPrice")} + className="w-full"> +
+ + + + +
+
+
+ + {apiConfiguration?.anthropicCustomModelInfo?.supportsPromptCache && ( + <> +
+ { + const value = apiConfiguration?.anthropicCustomModelInfo?.cacheReadsPrice + + if (!value && value !== 0) { + return "var(--vscode-input-border)" + } + + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={handleInputChange("anthropicCustomModelInfo", (e) => { + const value = (e.target as HTMLInputElement).value + const parsed = parseFloat(value) + + return { + ...(apiConfiguration?.anthropicCustomModelInfo ?? openAiModelInfoSaneDefaults), + cacheReadsPrice: isNaN(parsed) ? 0 : parsed, + } + })} + placeholder={t("settings:placeholders.numbers.inputPrice")} + className="w-full"> +
+ + {t("settings:providers.customModel.pricing.cacheReads.label")} + + + + +
+
+
+
+ { + const value = apiConfiguration?.anthropicCustomModelInfo?.cacheWritesPrice + + if (!value && value !== 0) { + return "var(--vscode-input-border)" + } + + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={handleInputChange("anthropicCustomModelInfo", (e) => { + const value = (e.target as HTMLInputElement).value + const parsed = parseFloat(value) + + return { + ...(apiConfiguration?.anthropicCustomModelInfo ?? openAiModelInfoSaneDefaults), + cacheWritesPrice: isNaN(parsed) ? 0 : parsed, + } + })} + placeholder={t("settings:placeholders.numbers.cacheWritePrice")} + className="w-full"> +
+ + + + +
+
+
+ + )} + + +
+ + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index d5dd0d0ded..23b9bc7859 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -1,4 +1,5 @@ export { Anthropic } from "./Anthropic" +export { AnthropicCustom } from "./AnthropicCustom" export { Bedrock } from "./Bedrock" export { DeepSeek } from "./DeepSeek" export { Gemini } from "./Gemini" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index d3ebb6c0dd..b14d13c6d8 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -370,8 +370,15 @@ function getSelectedModel({ // case "anthropic": // case "fake-ai": default: { - provider satisfies "anthropic" | "gemini-cli" | "fake-ai" - const id = apiConfiguration.apiModelId ?? defaultModelId + provider satisfies "anthropic" | "anthropic-custom" | "gemini-cli" | "fake-ai" + const id = apiConfiguration.apiModelId ?? apiConfiguration.anthropicCustomModelId ?? defaultModelId + + // For anthropic-custom, use custom model info if available + if (provider === "anthropic-custom") { + const info = apiConfiguration.anthropicCustomModelInfo || undefined + return { id, info } + } + const baseInfo = anthropicModels[id as keyof typeof anthropicModels] // Apply 1M context beta tier pricing for supported Claude 4 models diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 3de6480802..e494a7ce3f 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -97,6 +97,15 @@ function validateModelsAndKeysProvided( return i18next.t("settings:validation.openAi") } break + case "anthropic-custom": + if ( + !apiConfiguration.anthropicCustomBaseUrl || + !apiConfiguration.anthropicCustomApiKey || + !apiConfiguration.anthropicCustomModelId + ) { + return i18next.t("settings:validation.openAi") + } + break case "ollama": if (!apiConfiguration.ollamaModelId) { return i18next.t("settings:validation.modelId") @@ -195,6 +204,10 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: Pro return apiConfiguration.vsCodeLmModelSelector?.id } + if (provider === "anthropic-custom") { + return apiConfiguration.anthropicCustomModelId + } + if (isCustomProvider(provider) || isFauxProvider(provider)) { return apiConfiguration.apiModelId } From 47ee0960b751b816d70491826b602f80248ece9f Mon Sep 17 00:00:00 2001 From: AlgerMusic Build Bot Date: Sun, 28 Jun 2026 17:02:03 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E7=AE=80=E5=8C=96=20?= =?UTF-8?q?Anthropic=20Custom=20=E4=BB=A5=E5=8C=B9=E9=85=8D=20Anthropic=20?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=95=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除自定义 Headers UI 和流式传输复选框 - 基础 URL 改为可选(复选框启用) - 模型选择器使用 anthropicModels 列表 - 自动从选中模型填充模型信息 - 校验只需要 API 密钥和模型 ID(基础 URL 可选) --- .../settings/providers/AnthropicCustom.tsx | 381 ++++++------------ webview-ui/src/utils/validate.ts | 11 +- 2 files changed, 123 insertions(+), 269 deletions(-) diff --git a/webview-ui/src/components/settings/providers/AnthropicCustom.tsx b/webview-ui/src/components/settings/providers/AnthropicCustom.tsx index b8c6fda117..0fddb890f0 100644 --- a/webview-ui/src/components/settings/providers/AnthropicCustom.tsx +++ b/webview-ui/src/components/settings/providers/AnthropicCustom.tsx @@ -1,14 +1,19 @@ -import { useState, useCallback, useEffect } from "react" +import { useCallback, useEffect, useState } from "react" import { Checkbox } from "vscrui" -import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { type ProviderSettings, type OrganizationAllowList, openAiModelInfoSaneDefaults } from "@roo-code/types" +import { + type ProviderSettings, + type OrganizationAllowList, + anthropicModels, + openAiModelInfoSaneDefaults, +} from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { Button, StandardTooltip } from "@src/components/ui" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" -import { convertHeadersToObject } from "../utils/headers" -import { inputEventTransform, noTransform } from "../transforms" +import { inputEventTransform } from "../transforms" import { ModelPicker } from "../ModelPicker" type AnthropicCustomProps = { @@ -23,6 +28,8 @@ type AnthropicCustomProps = { simplifySettings?: boolean } +const anthropicCustomDefaultModelId = "claude-sonnet-4-5" + export const AnthropicCustom = ({ apiConfiguration, setApiConfigurationField, @@ -32,51 +39,20 @@ export const AnthropicCustom = ({ }: AnthropicCustomProps) => { const { t } = useAppTranslation() - const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { - const headers = apiConfiguration?.anthropicCustomHeaders || {} - return Object.entries(headers) - }) - - const handleAddCustomHeader = useCallback(() => { - setCustomHeaders((prev) => [...prev, ["", ""]]) - }, []) - - 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)) - }, []) + const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicCustomBaseUrl) useEffect(() => { - const timer = setTimeout(() => { - const headerObject = convertHeadersToObject(customHeaders) - setApiConfigurationField("anthropicCustomHeaders", headerObject, false) - }, 300) - - return () => clearTimeout(timer) - }, [customHeaders, setApiConfigurationField]) + if (!apiConfiguration.anthropicCustomModelInfo) { + setApiConfigurationField( + "anthropicCustomModelInfo", + { + ...openAiModelInfoSaneDefaults, + ...(anthropicModels[anthropicCustomDefaultModelId] || {}), + }, + false, + ) + } + }, [apiConfiguration.anthropicCustomModelInfo, setApiConfigurationField]) const handleInputChange = useCallback( ( @@ -89,80 +65,74 @@ export const AnthropicCustom = ({ [setApiConfigurationField], ) + const getCustomModelInfo = () => apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults + return ( <> - - - - + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.anthropicCustomApiKey && ( + + {t("settings:providers.getAnthropicApiKey")} + + )} +
+ { + setAnthropicBaseUrlSelected(checked) + + if (!checked) { + setApiConfigurationField("anthropicCustomBaseUrl", "") + } + }}> + {t("settings:providers.useCustomBaseUrl")} + + {anthropicBaseUrlSelected && ( + + + + )} +
{ + setApiConfigurationField(field, value, isUserAction) + + if (field === "anthropicCustomModelId") { + setApiConfigurationField( + "anthropicCustomModelInfo", + { + ...openAiModelInfoSaneDefaults, + ...(anthropicModels[value as keyof typeof anthropicModels] || {}), + }, + false, + ) + } + }} + defaultModelId={anthropicCustomDefaultModelId} + models={anthropicModels} modelIdKey="anthropicCustomModelId" - serviceName="Anthropic Custom" + serviceName="Anthropic" serviceUrl="https://docs.anthropic.com" organizationAllowList={organizationAllowList} errorMessage={modelValidationError} simplifySettings={simplifySettings} /> - - {t("settings:modelInfo.enableStreaming")} - - - {/* Custom Headers UI */} -
-
- - - - - - -
- {!customHeaders.length ? ( -
- {t("settings:providers.noCustomHeaders")} -
- ) : ( - customHeaders.map(([key, value], index) => ( -
- handleUpdateHeaderKey(index, e.target.value)} - /> - handleUpdateHeaderValue(index, e.target.value)} - /> - - handleRemoveCustomHeader(index)}> - - - -
- )) - )} -
@@ -171,28 +141,13 @@ export const AnthropicCustom = ({
{ - const value = apiConfiguration?.anthropicCustomModelInfo?.maxTokens - - if (!value) { - return "var(--vscode-input-border)" - } - - return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" - })(), - }} onInput={handleInputChange("anthropicCustomModelInfo", (e) => { const value = parseInt((e.target as HTMLInputElement).value) return { - ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), + ...getCustomModelInfo(), maxTokens: isNaN(value) ? undefined : value, } })} @@ -209,30 +164,14 @@ export const AnthropicCustom = ({
{ - const value = apiConfiguration?.anthropicCustomModelInfo?.contextWindow - - if (!value) { - return "var(--vscode-input-border)" - } - - return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" - })(), - }} onInput={handleInputChange("anthropicCustomModelInfo", (e) => { - const value = (e.target as HTMLInputElement).value - const parsed = parseInt(value) + const value = parseInt((e.target as HTMLInputElement).value) return { - ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), - contextWindow: isNaN(parsed) ? openAiModelInfoSaneDefaults.contextWindow : parsed, + ...getCustomModelInfo(), + contextWindow: isNaN(value) ? openAiModelInfoSaneDefaults.contextWindow : value, } })} placeholder={t("settings:placeholders.numbers.contextWindow")} @@ -249,16 +188,11 @@ export const AnthropicCustom = ({
{ - return { - ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), - supportsImages: checked, - } - })}> + checked={getCustomModelInfo().supportsImages ?? false} + onChange={handleInputChange("anthropicCustomModelInfo", (checked) => ({ + ...getCustomModelInfo(), + supportsImages: checked, + }))}> {t("settings:providers.customModel.imageSupport.label")} @@ -270,21 +204,16 @@ export const AnthropicCustom = ({ />
-
- {t("settings:providers.customModel.imageSupport.description")} -
{ - return { - ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), - supportsPromptCache: checked, - } - })}> + checked={getCustomModelInfo().supportsPromptCache ?? false} + onChange={handleInputChange("anthropicCustomModelInfo", (checked) => ({ + ...getCustomModelInfo(), + supportsPromptCache: checked, + }))}> {t("settings:providers.customModel.promptCache.label")} @@ -294,36 +223,17 @@ export const AnthropicCustom = ({ />
-
- {t("settings:providers.customModel.promptCache.description")} -
{ - const value = apiConfiguration?.anthropicCustomModelInfo?.inputPrice - - if (!value && value !== 0) { - return "var(--vscode-input-border)" - } - - return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" - })(), - }} onChange={handleInputChange("anthropicCustomModelInfo", (e) => { - const value = (e.target as HTMLInputElement).value - const parsed = parseFloat(value) + const parsed = parseFloat((e.target as HTMLInputElement).value) return { - ...(apiConfiguration?.anthropicCustomModelInfo ?? openAiModelInfoSaneDefaults), + ...getCustomModelInfo(), inputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed, } })} @@ -345,29 +255,13 @@ export const AnthropicCustom = ({
{ - const value = apiConfiguration?.anthropicCustomModelInfo?.outputPrice - - if (!value && value !== 0) { - return "var(--vscode-input-border)" - } - - return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" - })(), - }} onChange={handleInputChange("anthropicCustomModelInfo", (e) => { - const value = (e.target as HTMLInputElement).value - const parsed = parseFloat(value) + const parsed = parseFloat((e.target as HTMLInputElement).value) return { - ...(apiConfiguration?.anthropicCustomModelInfo || openAiModelInfoSaneDefaults), + ...getCustomModelInfo(), outputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed, } })} @@ -387,90 +281,44 @@ export const AnthropicCustom = ({
- {apiConfiguration?.anthropicCustomModelInfo?.supportsPromptCache && ( + {getCustomModelInfo().supportsPromptCache && ( <>
{ - const value = apiConfiguration?.anthropicCustomModelInfo?.cacheReadsPrice - - if (!value && value !== 0) { - return "var(--vscode-input-border)" - } - - return value >= 0 - ? "var(--vscode-charts-green)" - : "var(--vscode-errorForeground)" - })(), - }} onChange={handleInputChange("anthropicCustomModelInfo", (e) => { - const value = (e.target as HTMLInputElement).value - const parsed = parseFloat(value) + const parsed = parseFloat((e.target as HTMLInputElement).value) return { - ...(apiConfiguration?.anthropicCustomModelInfo ?? openAiModelInfoSaneDefaults), + ...getCustomModelInfo(), cacheReadsPrice: isNaN(parsed) ? 0 : parsed, } })} placeholder={t("settings:placeholders.numbers.inputPrice")} className="w-full"> -
- - {t("settings:providers.customModel.pricing.cacheReads.label")} - - - - -
+ + {t("settings:providers.customModel.pricing.cacheReads.label")} +
{ - const value = apiConfiguration?.anthropicCustomModelInfo?.cacheWritesPrice - - if (!value && value !== 0) { - return "var(--vscode-input-border)" - } - - return value >= 0 - ? "var(--vscode-charts-green)" - : "var(--vscode-errorForeground)" - })(), - }} onChange={handleInputChange("anthropicCustomModelInfo", (e) => { - const value = (e.target as HTMLInputElement).value - const parsed = parseFloat(value) + const parsed = parseFloat((e.target as HTMLInputElement).value) return { - ...(apiConfiguration?.anthropicCustomModelInfo ?? openAiModelInfoSaneDefaults), + ...getCustomModelInfo(), cacheWritesPrice: isNaN(parsed) ? 0 : parsed, } })} placeholder={t("settings:placeholders.numbers.cacheWritePrice")} className="w-full"> -
- - - - -
+
@@ -478,7 +326,14 @@ export const AnthropicCustom = ({
diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index e494a7ce3f..06bd63e237 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -98,12 +98,11 @@ function validateModelsAndKeysProvided( } break case "anthropic-custom": - if ( - !apiConfiguration.anthropicCustomBaseUrl || - !apiConfiguration.anthropicCustomApiKey || - !apiConfiguration.anthropicCustomModelId - ) { - return i18next.t("settings:validation.openAi") + if (!apiConfiguration.anthropicCustomApiKey) { + return i18next.t("settings:validation.apiKey") + } + if (!apiConfiguration.anthropicCustomModelId) { + return i18next.t("settings:validation.modelId") } break case "ollama":