Skip to content
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
18 changes: 15 additions & 3 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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") })),
Expand Down Expand Up @@ -463,6 +473,7 @@ export const providerSettingsSchema = z.object({
...bedrockSchema.shape,
...vertexSchema.shape,
...openAiSchema.shape,
...anthropicCustomSchema.shape,
...ollamaSchema.shape,
...vsCodeLmSchema.shape,
...lmStudioSchema.shape,
Expand Down Expand Up @@ -512,6 +523,7 @@ export const modelIdKeys = [
"apiModelId",
"openRouterModelId",
"openAiModelId",
"anthropicCustomModelId",
"ollamaModelId",
"lmStudioModelId",
"lmStudioDraftModelId",
Expand Down Expand Up @@ -575,7 +587,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
*/

// 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)) {
Expand Down Expand Up @@ -615,7 +627,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
*/

export const MODELS_BY_PROVIDER: Record<
Exclude<ProviderName, "fake-ai" | "gemini-cli" | "openai">,
Exclude<ProviderName, "fake-ai" | "gemini-cli" | "openai" | "anthropic-custom">,
{ id: ProviderName; label: string; models: string[] }
> = {
anthropic: {
Expand Down
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 17 additions & 6 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type AnthropicModelId,
anthropicDefaultModelId,
anthropicModels,
openAiModelInfoSaneDefaults,
ANTHROPIC_DEFAULT_MAX_TOKENS,
ApiProviderError,
} from "@roo-code/types"
Expand Down Expand Up @@ -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,
Comment on lines +42 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep anthropic-custom isolated from the standard Anthropic connection fields.

This fallback chain mixes the two provider namespaces. If a profile still has anthropicBaseUrl populated from the standard Anthropic setup, selecting anthropic-custom with its custom base URL disabled will still route prompts to that old proxy instead of Anthropic’s default endpoint. The same bleed-through can happen with credentials because buildApiHandler() no longer passes apiProvider into the handler, so this constructor has no way to tell which namespace should win. Please derive endpoint/auth strictly from the active provider instead of falling back across anthropic* and anthropicCustom*.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/anthropic.ts` around lines 42 - 52, The Anthropic client
setup is mixing standard and custom provider settings, so `AnthropicApiHandler`
should derive `baseURL`, auth field selection, and headers strictly from the
active provider namespace instead of falling back between `anthropic*` and
`anthropicCustom*`. Update the constructor logic in
`src/api/providers/anthropic.ts` so `anthropic-custom` only reads
`anthropicCustomBaseUrl`, `anthropicCustomApiKey`, and `anthropicCustomHeaders`,
while the standard Anthropic path only uses the non-custom fields; avoid using
the old fallback chain in `this.client = new Anthropic(...)`, and make sure any
provider selection passed from `buildApiHandler()` is used to choose the correct
source of truth.

timeout: this.timeoutMs,
})
}
Expand Down Expand Up @@ -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]
Comment on lines +361 to +368

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fallback to built-in Anthropic metadata for known custom model IDs.

anthropicCustomModelInfo is optional, but this path drops straight to openAiModelInfoSaneDefaults whenever a custom model ID is set. For any saved/imported profile that picks a stock Claude model but lacks the hydrated anthropicCustomModelInfo, you'll silently lose the real context window, prompt-cache support, and pricing. Please seed from anthropicModels[customModelId] when the ID is known, then layer user overrides on top.

Suggested patch
 		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]
+		const builtInCustomInfo =
+			customModelId && customModelId in anthropicModels
+				? anthropicModels[customModelId as AnthropicModelId]
+				: undefined
+		let info: ModelInfo = customModelId
+			? {
+					...(builtInCustomInfo ?? openAiModelInfoSaneDefaults),
+					...(this.options.anthropicCustomModelInfo ?? {}),
+				}
+			: anthropicModels[id as AnthropicModelId]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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]
const customModelId = this.options.anthropicCustomModelId
const modelId = customModelId || this.options.apiModelId
const id =
customModelId ||
(modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId)
const builtInCustomInfo =
customModelId && customModelId in anthropicModels
? anthropicModels[customModelId as AnthropicModelId]
: undefined
let info: ModelInfo = customModelId
? {
...(builtInCustomInfo ?? openAiModelInfoSaneDefaults),
...(this.options.anthropicCustomModelInfo ?? {}),
}
: anthropicModels[id as AnthropicModelId]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/anthropic.ts` around lines 361 - 368, The model metadata
fallback in the Anthropics provider is skipping built-in metadata whenever
anthropicCustomModelId is set, so saved profiles can lose the real Claude
defaults. Update the logic around the custom model path in this block to first
look up anthropicModels[customModelId] when the ID is a known Anthropic model,
then merge anthropicCustomModelInfo over that, and only use
openAiModelInfoSaneDefaults when no built-in metadata exists. Keep the change
localized to the custom model handling in the Anthropics provider where
modelId/id/info are computed.


// If 1M context beta is enabled for supported models, update the model info
if (
Expand Down
2 changes: 2 additions & 0 deletions src/shared/ProfileValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
11 changes: 11 additions & 0 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {

import {
Anthropic,
AnthropicCustom,
Baseten,
Bedrock,
DeepSeek,
Expand Down Expand Up @@ -468,6 +469,16 @@ const ApiOptions = ({
/>
)}

{selectedProvider === "anthropic-custom" && (
<AnthropicCustom
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}

{selectedProvider === "openai-codex" && (
<OpenAICodex
apiConfiguration={apiConfiguration}
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/components/settings/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ModelIdKey = keyof Pick<
| "requestyModelId"
| "unboundModelId"
| "openAiModelId"
| "anthropicCustomModelId"
| "litellmModelId"
| "vercelAiGatewayModelId"
| "opencodeGoModelId"
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/components/settings/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
export const PROVIDERS = [
{ value: "openrouter", label: "OpenRouter", proxy: false },
{ value: "anthropic", label: "Anthropic", proxy: false },
{ value: "anthropic-custom", label: "Anthropic Custom", proxy: true },
{ value: "gemini", label: "Google Gemini", proxy: false },
{ value: "deepseek", label: "DeepSeek", proxy: false },
{ value: "moonshot", label: "Moonshot", proxy: false },
Expand Down
Loading
Loading