feat: show retired-provider message for removed provider profiles

Preserve API profiles that reference removed providers instead of
silently stripping their apiProvider. When a user selects a profile
configured for a retired provider, the settings UI now shows an
empathetic message explaining the removal instead of the provider
configuration form.

- Add retiredProviderNames array and isRetiredProvider() helper to
  packages/types/src/provider-settings.ts
- Update ProviderSettingsManager sanitization to preserve retired
  providers (only strip truly unknown values)
- Update ContextProxy sanitization to preserve retired providers
- Render retired-provider message in ApiOptions.tsx when selected
  provider is in the retired list
- Add tests for sanitization, ContextProxy, and UI behavior
This commit is contained in:
Hannes Rudolph 2026-02-07 19:43:44 -07:00
parent e8158b4286
commit aebcaa263e
16 changed files with 731 additions and 355 deletions

View file

@ -129,6 +129,33 @@ export type ProviderName = z.infer<typeof providerNamesSchema>
export const isProviderName = (key: unknown): key is ProviderName =>
typeof key === "string" && providerNames.includes(key as ProviderName)
/**
* RetiredProviderName
*/
export const retiredProviderNames = [
"cerebras",
"chutes",
"deepinfra",
"doubao",
"featherless",
"groq",
"huggingface",
"io-intelligence",
"unbound",
] as const
export const retiredProviderNamesSchema = z.enum(retiredProviderNames)
export type RetiredProviderName = z.infer<typeof retiredProviderNamesSchema>
export const isRetiredProvider = (value: string): value is RetiredProviderName =>
retiredProviderNames.includes(value as RetiredProviderName)
export const providerNamesWithRetiredSchema = z.union([providerNamesSchema, retiredProviderNamesSchema])
export type ProviderNameWithRetired = z.infer<typeof providerNamesWithRetiredSchema>
/**
* ProviderSettingsEntry
*/
@ -136,7 +163,7 @@ export const isProviderName = (key: unknown): key is ProviderName =>
export const providerSettingsEntrySchema = z.object({
id: z.string(),
name: z.string(),
apiProvider: providerNamesSchema.optional(),
apiProvider: providerNamesWithRetiredSchema.optional(),
modelId: z.string().optional(),
})
@ -386,7 +413,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
])
export const providerSettingsSchema = z.object({
apiProvider: providerNamesSchema.optional(),
apiProvider: providerNamesWithRetiredSchema.optional(),
...anthropicSchema.shape,
...openRouterSchema.shape,
...bedrockSchema.shape,

View file

@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
import { isRetiredProvider, type ProviderSettings, type ModelInfo } from "@roo-code/types"
import { ApiStream } from "./transform/stream"
@ -119,6 +119,12 @@ export interface ApiHandler {
export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
const { apiProvider, ...options } = configuration
if (apiProvider && isRetiredProvider(apiProvider)) {
throw new Error(
`Sorry, this provider is no longer supported. We saw very few Roo users actually using it and we need to reduce the surface area of our codebase so we can keep shipping fast and serving our community well in this space. It was a really hard decision but it lets us focus on what matters most to you. It sucks, we know.\n\nPlease select a different provider in your API profile settings.`,
)
}
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)

View file

@ -16,6 +16,7 @@ import {
globalSettingsSchema,
isSecretStateKey,
isProviderName,
isRetiredProvider,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
@ -223,14 +224,16 @@ export class ContextProxy {
}
/**
* Migrates invalid/removed apiProvider values by clearing them from storage.
* This handles cases where a user had a provider selected that was later removed
* from the extension (e.g., "glama").
* Migrates unknown apiProvider values by clearing them from storage.
* Retired providers are preserved so users can keep historical configuration.
*/
private async migrateInvalidApiProvider() {
try {
const apiProvider = this.stateCache.apiProvider
if (apiProvider !== undefined && !isProviderName(apiProvider)) {
const isKnownProvider =
typeof apiProvider === "string" && (isProviderName(apiProvider) || isRetiredProvider(apiProvider))
if (apiProvider !== undefined && !isKnownProvider) {
logger.info(`[ContextProxy] Found invalid provider "${apiProvider}" in storage - clearing it`)
// Clear the invalid provider from both cache and storage
this.stateCache.apiProvider = undefined
@ -439,8 +442,8 @@ export class ContextProxy {
}
/**
* Sanitizes provider values by resetting invalid/removed apiProvider values.
* This prevents schema validation errors for removed providers.
* Sanitizes provider values by resetting unknown apiProvider values.
* Active and retired providers are preserved.
*/
private sanitizeProviderValues(values: RooCodeSettings): RooCodeSettings {
// Remove legacy Claude Code CLI wrapper keys that may still exist in global state.
@ -456,7 +459,11 @@ export class ContextProxy {
}
}
if (values.apiProvider !== undefined && !isProviderName(values.apiProvider)) {
const isKnownProvider =
typeof values.apiProvider === "string" &&
(isProviderName(values.apiProvider) || isRetiredProvider(values.apiProvider))
if (values.apiProvider !== undefined && !isKnownProvider) {
logger.info(`[ContextProxy] Sanitizing invalid provider "${values.apiProvider}" - resetting to undefined`)
// Return a new values object without the invalid apiProvider
const { apiProvider, ...restValues } = sanitizedValues

View file

@ -12,6 +12,7 @@ import {
getModelId,
type ProviderName,
isProviderName,
isRetiredProvider,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
@ -359,8 +360,12 @@ export class ProviderSettingsManager {
const existingId = providerProfiles.apiConfigs[name]?.id
const id = config.id || existingId || this.generateId()
// Filter out settings from other providers.
const filteredConfig = discriminatedProviderSettingsWithIdSchema.parse(config)
// For active providers, filter out settings from other providers.
// For retired providers, preserve full profile fields to avoid data loss.
const filteredConfig =
typeof config.apiProvider === "string" && isRetiredProvider(config.apiProvider)
? providerSettingsWithIdSchema.parse(config)
: discriminatedProviderSettingsWithIdSchema.parse(config)
providerProfiles.apiConfigs[name] = { ...filteredConfig, id }
await this.store(providerProfiles)
return id
@ -507,7 +512,14 @@ export class ProviderSettingsManager {
const profiles = providerProfilesSchema.parse(await this.load())
const configs = profiles.apiConfigs
for (const name in configs) {
// Avoid leaking properties from other providers.
const apiProvider = configs[name].apiProvider
if (typeof apiProvider === "string" && isRetiredProvider(apiProvider)) {
// Preserve retired-provider profiles as-is to prevent dropping legacy fields.
continue
}
// Avoid leaking properties from other active providers.
configs[name] = discriminatedProviderSettingsWithIdSchema.parse(configs[name])
// If it has no apiProvider, skip filtering
@ -607,7 +619,8 @@ export class ProviderSettingsManager {
}
/**
* Sanitizes a provider config by resetting invalid/removed apiProvider values.
* Sanitizes a provider config by resetting unknown apiProvider values.
* Retired providers are preserved.
* This handles cases where a user had a provider selected that was later removed
* from the extension (e.g., "glama").
*/
@ -618,10 +631,15 @@ export class ProviderSettingsManager {
const config = apiConfig as Record<string, unknown>
// Check if apiProvider is set and if it's still valid
if (config.apiProvider !== undefined && !isProviderName(config.apiProvider)) {
const apiProvider = config.apiProvider
// Check if apiProvider is set and if it's still recognized (active or retired)
if (
apiProvider !== undefined &&
(typeof apiProvider !== "string" || (!isProviderName(apiProvider) && !isRetiredProvider(apiProvider)))
) {
console.log(
`[ProviderSettingsManager] Sanitizing invalid provider "${config.apiProvider}" - resetting to undefined`,
`[ProviderSettingsManager] Sanitizing unknown provider "${config.apiProvider}" - resetting to undefined`,
)
// Return a new config object without the invalid apiProvider
// This effectively resets the profile so the user can select a valid provider

View file

@ -424,7 +424,7 @@ describe("ContextProxy", () => {
it("should reinitialize caches after reset", async () => {
// Spy on initialization methods
const initializeSpy = vi.spyOn(proxy as any, "initialize")
const initializeSpy = vi.spyOn(proxy, "initialize")
// Reset all state
await proxy.resetAllState()
@ -452,6 +452,25 @@ describe("ContextProxy", () => {
expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", undefined)
})
it("should not clear retired apiProvider from storage during initialization", async () => {
// Reset and create a new proxy with retired provider in state
vi.clearAllMocks()
mockGlobalState.get.mockImplementation((key: string) => {
if (key === "apiProvider") {
return "groq" // Retired provider
}
return undefined
})
const proxyWithRetiredProvider = new ContextProxy(mockContext)
await proxyWithRetiredProvider.initialize()
// Should NOT have called update for apiProvider (retired should be preserved)
const updateCalls = mockGlobalState.update.mock.calls
const apiProviderUpdateCalls = updateCalls.filter((call: unknown[]) => call[0] === "apiProvider")
expect(apiProviderUpdateCalls).toHaveLength(0)
})
it("should not modify valid apiProvider during initialization", async () => {
// Reset and create a new proxy with valid provider in state
vi.clearAllMocks()
@ -467,18 +486,29 @@ describe("ContextProxy", () => {
// Should NOT have called update for apiProvider (it's valid)
const updateCalls = mockGlobalState.update.mock.calls
const apiProviderUpdateCalls = updateCalls.filter((call: any[]) => call[0] === "apiProvider")
const apiProviderUpdateCalls = updateCalls.filter((call: unknown[]) => call[0] === "apiProvider")
expect(apiProviderUpdateCalls.length).toBe(0)
})
})
describe("getProviderSettings", () => {
it("should sanitize invalid apiProvider before parsing", async () => {
// Set an invalid provider in state
await proxy.updateGlobalState("apiProvider", "invalid-removed-provider" as any)
await proxy.updateGlobalState("apiModelId", "some-model")
// Reset and create a new proxy with an unknown provider in state
vi.clearAllMocks()
mockGlobalState.get.mockImplementation((key: string) => {
if (key === "apiProvider") {
return "invalid-removed-provider"
}
if (key === "apiModelId") {
return "some-model"
}
return undefined
})
const settings = proxy.getProviderSettings()
const proxyWithInvalidProvider = new ContextProxy(mockContext)
await proxyWithInvalidProvider.initialize()
const settings = proxyWithInvalidProvider.getProviderSettings()
// The invalid apiProvider should be sanitized (removed)
expect(settings.apiProvider).toBeUndefined()
@ -486,6 +516,22 @@ describe("ContextProxy", () => {
expect(settings.apiModelId).toBe("some-model")
})
it("should preserve retired apiProvider and provider fields", async () => {
await proxy.setValues({
apiProvider: "groq",
apiModelId: "llama3-70b",
openAiBaseUrl: "https://api.retired-provider.example/v1",
apiKey: "retired-provider-key",
})
const settings = proxy.getProviderSettings()
expect(settings.apiProvider).toBe("groq")
expect(settings.apiModelId).toBe("llama3-70b")
expect(settings.openAiBaseUrl).toBe("https://api.retired-provider.example/v1")
expect(settings.apiKey).toBe("retired-provider-key")
})
it("should pass through valid apiProvider", async () => {
// Set a valid provider in state
await proxy.updateGlobalState("apiProvider", "anthropic")

View file

@ -566,6 +566,42 @@ describe("ProviderSettingsManager", () => {
"Failed to save config: Error: Failed to write provider profiles to secrets: Error: Storage failed",
)
})
it("should preserve full fields when saving retired provider profiles", async () => {
mockSecrets.get.mockResolvedValue(
JSON.stringify({
currentApiConfigName: "default",
apiConfigs: {
default: {},
},
modeApiConfigs: {
code: "default",
architect: "default",
ask: "default",
},
}),
)
const retiredConfig: ProviderSettings = {
apiProvider: "groq",
apiKey: "legacy-key",
apiModelId: "legacy-model",
openAiBaseUrl: "https://legacy.example/v1",
openAiApiKey: "legacy-openai-key",
modelMaxTokens: 4096,
}
await providerSettingsManager.saveConfig("retired", retiredConfig)
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[mockSecrets.store.mock.calls.length - 1][1])
expect(storedConfig.apiConfigs.retired.apiProvider).toBe("groq")
expect(storedConfig.apiConfigs.retired.apiKey).toBe("legacy-key")
expect(storedConfig.apiConfigs.retired.apiModelId).toBe("legacy-model")
expect(storedConfig.apiConfigs.retired.openAiBaseUrl).toBe("https://legacy.example/v1")
expect(storedConfig.apiConfigs.retired.openAiApiKey).toBe("legacy-openai-key")
expect(storedConfig.apiConfigs.retired.modelMaxTokens).toBe(4096)
expect(storedConfig.apiConfigs.retired.id).toBeTruthy()
})
})
describe("DeleteConfig", () => {
@ -695,9 +731,9 @@ describe("ProviderSettingsManager", () => {
)
})
it("should sanitize invalid/removed providers by resetting apiProvider to undefined", async () => {
it("should sanitize unknown providers by resetting apiProvider to undefined", async () => {
// This tests the fix for the infinite loop issue when a provider is removed
const configWithRemovedProvider = {
const configWithUnknownProvider = {
currentApiConfigName: "valid",
apiConfigs: {
valid: {
@ -706,8 +742,8 @@ describe("ProviderSettingsManager", () => {
apiModelId: "claude-3-opus-20240229",
id: "valid-id",
},
removedProvider: {
// Provider that was removed from the extension (e.g., "invalid-removed-provider")
unknownProvider: {
// Provider value that is neither active nor retired.
id: "removed-id",
apiProvider: "invalid-removed-provider",
apiKey: "some-key",
@ -722,7 +758,7 @@ describe("ProviderSettingsManager", () => {
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(configWithRemovedProvider))
mockSecrets.get.mockResolvedValue(JSON.stringify(configWithUnknownProvider))
await providerSettingsManager.initialize()
@ -735,11 +771,51 @@ describe("ProviderSettingsManager", () => {
expect(storedConfig.apiConfigs.valid).toBeDefined()
expect(storedConfig.apiConfigs.valid.apiProvider).toBe("anthropic")
// The config with the removed provider should have its apiProvider reset to undefined
// The config with the unknown provider should have its apiProvider reset to undefined
// but still be present (not filtered out entirely)
expect(storedConfig.apiConfigs.removedProvider).toBeDefined()
expect(storedConfig.apiConfigs.removedProvider.apiProvider).toBeUndefined()
expect(storedConfig.apiConfigs.removedProvider.id).toBe("removed-id")
expect(storedConfig.apiConfigs.unknownProvider).toBeDefined()
expect(storedConfig.apiConfigs.unknownProvider.apiProvider).toBeUndefined()
expect(storedConfig.apiConfigs.unknownProvider.id).toBe("removed-id")
})
it("should preserve retired providers and their fields during initialize", async () => {
const configWithRetiredProvider = {
currentApiConfigName: "retiredProvider",
apiConfigs: {
retiredProvider: {
id: "retired-id",
apiProvider: "groq",
apiKey: "legacy-key",
apiModelId: "legacy-model",
openAiBaseUrl: "https://legacy.example/v1",
modelMaxTokens: 1024,
},
},
migrations: {
rateLimitSecondsMigrated: false,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: true,
todoListEnabledMigrated: true,
claudeCodeLegacySettingsMigrated: true,
},
}
mockGlobalState.get.mockResolvedValue(0)
mockSecrets.get.mockResolvedValue(JSON.stringify(configWithRetiredProvider))
await providerSettingsManager.initialize()
const storeCalls = mockSecrets.store.mock.calls
expect(storeCalls.length).toBeGreaterThan(0)
const finalStoredConfigJson = storeCalls[storeCalls.length - 1][1]
const storedConfig = JSON.parse(finalStoredConfigJson)
expect(storedConfig.apiConfigs.retiredProvider).toBeDefined()
expect(storedConfig.apiConfigs.retiredProvider.apiProvider).toBe("groq")
expect(storedConfig.apiConfigs.retiredProvider.apiKey).toBe("legacy-key")
expect(storedConfig.apiConfigs.retiredProvider.apiModelId).toBe("legacy-model")
expect(storedConfig.apiConfigs.retiredProvider.openAiBaseUrl).toBe("https://legacy.example/v1")
expect(storedConfig.apiConfigs.retiredProvider.modelMaxTokens).toBe(1024)
})
it("should sanitize invalid providers and remove non-object profiles during load", async () => {
@ -791,6 +867,36 @@ describe("ProviderSettingsManager", () => {
})
})
describe("Export", () => {
it("should preserve retired provider profiles with full fields", async () => {
const existingConfig: ProviderProfiles = {
currentApiConfigName: "retired",
apiConfigs: {
retired: {
id: "retired-id",
apiProvider: "groq",
apiKey: "legacy-key",
apiModelId: "legacy-model",
openAiBaseUrl: "https://legacy.example/v1",
modelMaxTokens: 4096,
modelMaxThinkingTokens: 2048,
},
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
const exported = await providerSettingsManager.export()
expect(exported.apiConfigs.retired.apiProvider).toBe("groq")
expect(exported.apiConfigs.retired.apiKey).toBe("legacy-key")
expect(exported.apiConfigs.retired.apiModelId).toBe("legacy-model")
expect(exported.apiConfigs.retired.openAiBaseUrl).toBe("https://legacy.example/v1")
expect(exported.apiConfigs.retired.modelMaxTokens).toBe(4096)
expect(exported.apiConfigs.retired.modelMaxThinkingTokens).toBe(2048)
})
})
describe("ResetAllConfigs", () => {
it("should delete all stored configs", async () => {
// Setup initial config

View file

@ -41,6 +41,7 @@ import {
TodoItem,
getApiProtocol,
getModelId,
isRetiredProvider,
isIdleAsk,
isInteractiveAsk,
isResumableAsk,
@ -1035,7 +1036,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Other providers (notably Gemini 3) use different signature semantics (e.g. `thoughtSignature`)
// and require round-tripping the signature in their own format.
const modelId = getModelId(this.apiConfiguration)
const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId)
const apiProvider = this.apiConfiguration.apiProvider
const apiProtocol = getApiProtocol(
apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId,
)
const isAnthropicProtocol = apiProtocol === "anthropic"
// Start from the original assistant message
@ -2695,7 +2700,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Determine API protocol based on provider and model
const modelId = getModelId(this.apiConfiguration)
const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId)
const apiProvider = this.apiConfiguration.apiProvider
const apiProtocol = getApiProtocol(
apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId,
)
// Respect user-configured provider rate limiting BEFORE we emit api_req_started.
// This prevents the UI from showing an "API Request..." spinner while we are
@ -2816,7 +2825,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Calculate total tokens and cost using provider-aware function
const modelId = getModelId(this.apiConfiguration)
const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId)
const apiProvider = this.apiConfiguration.apiProvider
const apiProtocol = getApiProtocol(
apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId,
)
const costResult =
apiProtocol === "anthropic"
@ -3140,7 +3153,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Capture telemetry with provider-aware cost calculation
const modelId = getModelId(this.apiConfiguration)
const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId)
const apiProvider = this.apiConfiguration.apiProvider
const apiProtocol = getApiProtocol(
apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId,
)
// Use the appropriate cost function based on the API protocol
const costResult =

View file

@ -45,6 +45,7 @@ import {
DEFAULT_MODES,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
getModelId,
isRetiredProvider,
} from "@roo-code/types"
import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts"
import { TelemetryService } from "@roo-code/telemetry"
@ -2266,8 +2267,11 @@ export class ClineProvider
const stateValues = this.contextProxy.getValues()
const customModes = await this.customModesManager.getCustomModes()
// Determine apiProvider with the same logic as before.
const apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : "anthropic"
// Determine apiProvider with the same logic as before, while filtering retired providers.
const apiProvider: ProviderName =
stateValues.apiProvider && !isRetiredProvider(stateValues.apiProvider)
? stateValues.apiProvider
: "anthropic"
// Build the apiConfiguration object combining state values and secrets.
const providerSettings = this.contextProxy.getProviderSettings()
@ -3105,12 +3109,14 @@ export class ClineProvider
}
}
const apiProvider = apiConfiguration?.apiProvider
return {
language,
mode,
taskId: task?.taskId,
parentTaskId: task?.parentTaskId,
apiProvider: apiConfiguration?.apiProvider,
apiProvider: apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId: task?.api?.getModel().id,
diffStrategy: task?.diffStrategy?.getName(),
isSubtask: task ? !!task.parentTaskId : undefined,

View file

@ -43,6 +43,7 @@ import { ChatTextArea } from "./ChatTextArea"
import TaskHeader from "./TaskHeader"
import SystemPromptWarning from "./SystemPromptWarning"
import ProfileViolationWarning from "./ProfileViolationWarning"
import RetiredProviderWarning from "./RetiredProviderWarning"
import { CheckpointWarning } from "./CheckpointWarning"
import { QueuedMessages } from "./QueuedMessages"
import { WorktreeSelector } from "./WorktreeSelector"
@ -1779,6 +1780,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
onEnqueueMessage={handleEnqueueCurrentMessage}
/>
<div className="px-3">
<RetiredProviderWarning />
</div>
{isProfileDisabled && (
<div className="px-3">
<ProfileViolationWarning />

View file

@ -0,0 +1,34 @@
import React from "react"
import { isRetiredProvider } from "@roo-code/types"
import { useExtensionState } from "@src/context/ExtensionStateContext"
export const RetiredProviderWarning: React.FC = () => {
const { apiConfiguration } = useExtensionState()
const provider = apiConfiguration?.apiProvider
if (!provider || !isRetiredProvider(provider)) {
return null
}
return (
<div className="flex flex-col gap-1.5 px-4 py-3 mb-2 text-sm rounded border border-vscode-inputValidation-warningBorder bg-vscode-inputValidation-warningBackground text-vscode-foreground">
<div className="flex items-center gap-2 font-bold">
<span className="codicon codicon-warning text-vscode-editorWarning-foreground" />
<span>Provider No Longer Supported</span>
</div>
<p className="m-0 leading-relaxed">
Sorry, this provider is no longer supported. We saw very few Roo users actually using it and we need to
reduce the surface area of our codebase so we can keep shipping fast and serving our community well in
this space. It was a really hard decision but it lets us focus on what matters most to you. It sucks, we
know.
</p>
<p className="m-0 leading-relaxed font-medium">
Please select a different provider in your API profile settings.
</p>
</div>
)
}
export default RetiredProviderWarning

View file

@ -7,6 +7,7 @@ import { ExternalLinkIcon } from "@radix-ui/react-icons"
import {
type ProviderName,
type ProviderSettings,
isRetiredProvider,
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
openRouterDefaultModelId,
requestyDefaultModelId,
@ -179,6 +180,11 @@ const ApiOptions = ({
id: selectedModelId,
info: selectedModelInfo,
} = useSelectedModel(apiConfiguration)
const activeSelectedProvider: ProviderName | undefined = isRetiredProvider(selectedProvider)
? undefined
: selectedProvider
const isRetiredSelectedProvider =
typeof apiConfiguration.apiProvider === "string" && isRetiredProvider(apiConfiguration.apiProvider)
const { data: routerModels, refetch: refetchRouterModels } = useRouterModels()
@ -196,12 +202,16 @@ const ApiOptions = ({
// Update `apiModelId` whenever `selectedModelId` changes.
useEffect(() => {
if (isRetiredSelectedProvider) {
return
}
if (selectedModelId && apiConfiguration.apiModelId !== selectedModelId) {
// Pass false as third parameter to indicate this is not a user action
// This is an internal sync, not a user-initiated change
setApiConfigurationField("apiModelId", selectedModelId, false)
}
}, [selectedModelId, setApiConfigurationField, apiConfiguration.apiModelId])
}, [selectedModelId, setApiConfigurationField, apiConfiguration.apiModelId, isRetiredSelectedProvider])
// Debounced refresh model updates, only executed 250ms after the user
// stops typing.
@ -245,13 +255,18 @@ const ApiOptions = ({
)
useEffect(() => {
if (isRetiredSelectedProvider) {
setErrorMessage(undefined)
return
}
const apiValidationResult = validateApiConfigurationExcludingModelErrors(
apiConfiguration,
routerModels,
organizationAllowList,
)
setErrorMessage(apiValidationResult)
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage])
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider])
const onProviderChange = useCallback(
(value: ProviderName) => {
@ -469,311 +484,358 @@ const ApiOptions = ({
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
{selectedProvider === "openrouter" && (
<OpenRouter
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
selectedModelId={selectedModelId}
uriScheme={uriScheme}
simplifySettings={fromWelcomeView}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
/>
)}
{selectedProvider === "requesty" && (
<Requesty
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
refetchRouterModels={refetchRouterModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "anthropic" && (
<Anthropic
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "openai-codex" && (
<OpenAICodex
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
openAiCodexIsAuthenticated={openAiCodexIsAuthenticated}
/>
)}
{selectedProvider === "openai-native" && (
<OpenAI
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "mistral" && (
<Mistral
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "baseten" && (
<Baseten
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "bedrock" && (
<Bedrock
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "vertex" && (
<Vertex apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "gemini" && (
<Gemini apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "openai" && (
<OpenAICompatible
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "lmstudio" && (
<LMStudio apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "deepseek" && (
<DeepSeek
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "qwen-code" && (
<QwenCode
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "moonshot" && (
<Moonshot
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "minimax" && (
<MiniMax apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "vscode-lm" && (
<VSCodeLM apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "ollama" && (
<Ollama apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "xai" && (
<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "litellm" && (
<LiteLLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "sambanova" && (
<SambaNova apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "zai" && (
<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "vercel-ai-gateway" && (
<VercelAiGateway
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "fireworks" && (
<Fireworks apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "roo" && (
<Roo
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
cloudIsAuthenticated={cloudIsAuthenticated}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{/* Generic model picker for providers with static models */}
{shouldUseGenericModelPicker(selectedProvider) && (
{isRetiredSelectedProvider ? (
<div
className="rounded-md border border-vscode-panel-border px-3 py-2 text-sm text-vscode-descriptionForeground"
data-testid="retired-provider-message">
Sorry, this provider is no longer supported. We saw very few Roo users actually using it and we need
to reduce the surface area of our codebase so we can keep shipping fast and serving our community
well in this space. It was a really hard decision but it lets us focus on what matters most to you.
It sucks, we know.
</div>
) : (
<>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={getDefaultModelIdForProvider(selectedProvider, apiConfiguration)}
models={getStaticModelsForProvider(selectedProvider, t("settings:labels.useCustomArn"))}
modelIdKey="apiModelId"
serviceName={getProviderServiceConfig(selectedProvider).serviceName}
serviceUrl={getProviderServiceConfig(selectedProvider).serviceUrl}
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
simplifySettings={fromWelcomeView}
onModelChange={(modelId) =>
handleModelChangeSideEffects(selectedProvider, modelId, setApiConfigurationField)
}
/>
{selectedProvider === "openrouter" && (
<OpenRouter
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
selectedModelId={selectedModelId}
uriScheme={uriScheme}
simplifySettings={fromWelcomeView}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
/>
)}
{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
<BedrockCustomArn
{selectedProvider === "requesty" && (
<Requesty
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
refetchRouterModels={refetchRouterModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "anthropic" && (
<Anthropic
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "openai-codex" && (
<OpenAICodex
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
openAiCodexIsAuthenticated={openAiCodexIsAuthenticated}
/>
)}
{selectedProvider === "openai-native" && (
<OpenAI
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "mistral" && (
<Mistral
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "baseten" && (
<Baseten
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "bedrock" && (
<Bedrock
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "vertex" && (
<Vertex
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
</>
)}
{!fromWelcomeView && (
<ThinkingBudget
key={`${selectedProvider}-${selectedModelId}`}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{/* Gate Verbosity UI by capability flag */}
{!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
<Verbosity
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{!fromWelcomeView && (
<Collapsible open={isAdvancedSettingsOpen} onOpenChange={setIsAdvancedSettingsOpen}>
<CollapsibleTrigger className="flex items-center gap-1 w-full cursor-pointer hover:opacity-80 mb-2">
<span className={`codicon codicon-chevron-${isAdvancedSettingsOpen ? "down" : "right"}`}></span>
<span className="font-medium">{t("settings:advancedSettings.title")}</span>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3">
<TodoListSettingsControl
todoListEnabled={apiConfiguration.todoListEnabled}
onChange={(field, value) => setApiConfigurationField(field, value)}
{selectedProvider === "gemini" && (
<Gemini
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
{selectedModelInfo?.supportsTemperature !== false && (
<TemperatureControl
value={apiConfiguration.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}
maxValue={2}
defaultValue={selectedModelInfo?.defaultTemperature}
)}
{selectedProvider === "openai" && (
<OpenAICompatible
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "lmstudio" && (
<LMStudio
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "deepseek" && (
<DeepSeek
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "qwen-code" && (
<QwenCode
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "moonshot" && (
<Moonshot
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "minimax" && (
<MiniMax
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "vscode-lm" && (
<VSCodeLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "ollama" && (
<Ollama
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "xai" && (
<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "litellm" && (
<LiteLLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "sambanova" && (
<SambaNova
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "zai" && (
<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "vercel-ai-gateway" && (
<VercelAiGateway
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "fireworks" && (
<Fireworks
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "roo" && (
<Roo
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
cloudIsAuthenticated={cloudIsAuthenticated}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{/* Generic model picker for providers with static models */}
{activeSelectedProvider && shouldUseGenericModelPicker(activeSelectedProvider) && (
<>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={getDefaultModelIdForProvider(activeSelectedProvider, apiConfiguration)}
models={getStaticModelsForProvider(
activeSelectedProvider,
t("settings:labels.useCustomArn"),
)}
modelIdKey="apiModelId"
serviceName={getProviderServiceConfig(activeSelectedProvider).serviceName}
serviceUrl={getProviderServiceConfig(activeSelectedProvider).serviceUrl}
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
simplifySettings={fromWelcomeView}
onModelChange={(modelId) =>
handleModelChangeSideEffects(
activeSelectedProvider,
modelId,
setApiConfigurationField,
)
}
/>
)}
<RateLimitSecondsControl
value={apiConfiguration.rateLimitSeconds || 0}
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
/>
<ConsecutiveMistakeLimitControl
value={
apiConfiguration.consecutiveMistakeLimit !== undefined
? apiConfiguration.consecutiveMistakeLimit
: DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
}
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
/>
{selectedProvider === "openrouter" &&
openRouterModelProviders &&
Object.keys(openRouterModelProviders).length > 0 && (
<div>
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.openRouter.providerRouting.title")}
</label>
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
<Select
value={
apiConfiguration?.openRouterSpecificProvider ||
OPENROUTER_DEFAULT_PROVIDER_NAME
}
onValueChange={(value) =>
setApiConfigurationField("openRouterSpecificProvider", value)
}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
{OPENROUTER_DEFAULT_PROVIDER_NAME}
</SelectItem>
{Object.entries(openRouterModelProviders).map(([value, { label }]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.openRouter.providerRouting.description")}{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">
{t("settings:providers.openRouter.providerRouting.learnMore")}.
</a>
</div>
</div>
{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
<BedrockCustomArn
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
</CollapsibleContent>
</Collapsible>
</>
)}
{!fromWelcomeView && (
<ThinkingBudget
key={`${selectedProvider}-${selectedModelId}`}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{/* Gate Verbosity UI by capability flag */}
{!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
<Verbosity
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{!fromWelcomeView && (
<Collapsible open={isAdvancedSettingsOpen} onOpenChange={setIsAdvancedSettingsOpen}>
<CollapsibleTrigger className="flex items-center gap-1 w-full cursor-pointer hover:opacity-80 mb-2">
<span
className={`codicon codicon-chevron-${isAdvancedSettingsOpen ? "down" : "right"}`}></span>
<span className="font-medium">{t("settings:advancedSettings.title")}</span>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3">
<TodoListSettingsControl
todoListEnabled={apiConfiguration.todoListEnabled}
onChange={(field, value) => setApiConfigurationField(field, value)}
/>
{selectedModelInfo?.supportsTemperature !== false && (
<TemperatureControl
value={apiConfiguration.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}
maxValue={2}
defaultValue={selectedModelInfo?.defaultTemperature}
/>
)}
<RateLimitSecondsControl
value={apiConfiguration.rateLimitSeconds || 0}
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
/>
<ConsecutiveMistakeLimitControl
value={
apiConfiguration.consecutiveMistakeLimit !== undefined
? apiConfiguration.consecutiveMistakeLimit
: DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
}
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
/>
{selectedProvider === "openrouter" &&
openRouterModelProviders &&
Object.keys(openRouterModelProviders).length > 0 && (
<div>
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.openRouter.providerRouting.title")}
</label>
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
<Select
value={
apiConfiguration?.openRouterSpecificProvider ||
OPENROUTER_DEFAULT_PROVIDER_NAME
}
onValueChange={(value) =>
setApiConfigurationField("openRouterSpecificProvider", value)
}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
{OPENROUTER_DEFAULT_PROVIDER_NAME}
</SelectItem>
{Object.entries(openRouterModelProviders).map(
([value, { label }]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
),
)}
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.openRouter.providerRouting.description")}{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">
{t("settings:providers.openRouter.providerRouting.learnMore")}.
</a>
</div>
</div>
)}
</CollapsibleContent>
</Collapsible>
)}
</>
)}
</div>
)

View file

@ -3,7 +3,7 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Trans } from "react-i18next"
import { ChevronsUpDown, Check, X, Info } from "lucide-react"
import type { ProviderSettings, ModelInfo, OrganizationAllowList } from "@roo-code/types"
import { type ProviderSettings, type ModelInfo, type OrganizationAllowList, isRetiredProvider } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
@ -104,8 +104,13 @@ export const ModelPicker = ({
return selectedModelId
}, [displayTransform, apiConfiguration, modelIdKey, selectedModelId])
const activeProvider =
apiConfiguration.apiProvider && isRetiredProvider(apiConfiguration.apiProvider)
? undefined
: apiConfiguration.apiProvider
const modelIds = useMemo(() => {
const filteredModels = filterModels(models, apiConfiguration.apiProvider, organizationAllowList)
const filteredModels = filterModels(models, activeProvider, organizationAllowList)
// Include the currently selected model even if deprecated (so users can see what they have selected)
// But filter out other deprecated models from being newly selectable
@ -125,7 +130,7 @@ export const ModelPicker = ({
)
return Object.keys(availableModels).sort((a, b) => a.localeCompare(b))
}, [models, apiConfiguration.apiProvider, organizationAllowList, selectedModelId])
}, [models, activeProvider, organizationAllowList, selectedModelId])
const [searchValue, setSearchValue] = useState("")

View file

@ -662,4 +662,31 @@ describe("ApiOptions", () => {
useExtensionStateMock.mockRestore()
})
})
it("renders retired provider message and hides provider-specific forms", () => {
renderApiOptions({
apiConfiguration: {
apiProvider: "groq",
},
})
expect(screen.getByTestId("retired-provider-message")).toHaveTextContent(
"Sorry, this provider is no longer supported. We saw very few Roo users actually using it and we need to reduce the surface area of our codebase so we can keep shipping fast and serving our community well in this space. It was a really hard decision but it lets us focus on what matters most to you. It sucks, we know.",
)
expect(screen.queryByTestId("litellm-provider")).not.toBeInTheDocument()
})
it("does not reintroduce retired providers into active provider options", () => {
renderApiOptions({
apiConfiguration: {
apiProvider: "groq",
},
})
const providerSelectContainer = screen.getByTestId("provider-select")
const providerSelect = providerSelectContainer.querySelector("select") as HTMLSelectElement
const providerOptions = Array.from(providerSelect.querySelectorAll("option")).map((option) => option.value)
expect(providerOptions).not.toContain("groq")
})
})

View file

@ -29,6 +29,7 @@ import {
BEDROCK_1M_CONTEXT_MODEL_IDS,
VERTEX_1M_CONTEXT_MODEL_IDS,
isDynamicProvider,
isRetiredProvider,
getProviderDefaultModelId,
} from "@roo-code/types"
@ -51,14 +52,16 @@ function getValidatedModelId(
export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
const provider = apiConfiguration?.apiProvider || "anthropic"
const openRouterModelId = provider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined
const lmStudioModelId = provider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined
const ollamaModelId = provider === "ollama" ? apiConfiguration?.ollamaModelId : undefined
const activeProvider: ProviderName | undefined = isRetiredProvider(provider) ? undefined : provider
const dynamicProvider = activeProvider && isDynamicProvider(activeProvider) ? activeProvider : undefined
const openRouterModelId = activeProvider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined
const lmStudioModelId = activeProvider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined
const ollamaModelId = activeProvider === "ollama" ? apiConfiguration?.ollamaModelId : undefined
// Only fetch router models for dynamic providers
const shouldFetchRouterModels = isDynamicProvider(provider)
const shouldFetchRouterModels = !!dynamicProvider
const routerModels = useRouterModels({
provider: shouldFetchRouterModels ? provider : undefined,
provider: dynamicProvider,
enabled: shouldFetchRouterModels,
})
@ -68,16 +71,17 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
// Compute readiness only for the data actually needed for the selected provider
const needRouterModels = shouldFetchRouterModels
const needOpenRouterProviders = provider === "openrouter"
const needOpenRouterProviders = activeProvider === "openrouter"
const needLmStudio = typeof lmStudioModelId !== "undefined"
const needOllama = typeof ollamaModelId !== "undefined"
const hasValidRouterData = needRouterModels
? routerModels.data &&
routerModels.data[provider] !== undefined &&
typeof routerModels.data[provider] === "object" &&
!routerModels.isLoading
: true
const hasValidRouterData =
needRouterModels && dynamicProvider
? routerModels.data &&
routerModels.data[dynamicProvider] !== undefined &&
typeof routerModels.data[dynamicProvider] === "object" &&
!routerModels.isLoading
: true
const isReady =
(!needLmStudio || typeof lmStudioModels.data !== "undefined") &&
@ -86,16 +90,16 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
(!needOpenRouterProviders || typeof openRouterModelProviders.data !== "undefined")
const { id, info } =
apiConfiguration && isReady
apiConfiguration && isReady && activeProvider
? getSelectedModel({
provider,
provider: activeProvider,
apiConfiguration,
routerModels: (routerModels.data || {}) as RouterModels,
openRouterModelProviders: (openRouterModelProviders.data || {}) as Record<string, ModelInfo>,
lmStudioModels: (lmStudioModels.data || undefined) as ModelRecord | undefined,
ollamaModels: (ollamaModels.data || undefined) as ModelRecord | undefined,
})
: { id: getProviderDefaultModelId(provider), info: undefined }
: { id: getProviderDefaultModelId(activeProvider ?? "anthropic"), info: undefined }
return {
provider,

View file

@ -131,6 +131,10 @@
--color-vscode-inputValidation-infoBackground: var(--vscode-inputValidation-infoBackground);
--color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
--color-vscode-inputValidation-warningForeground: var(--vscode-inputValidation-warningForeground);
--color-vscode-inputValidation-warningBackground: var(--vscode-inputValidation-warningBackground);
--color-vscode-inputValidation-warningBorder: var(--vscode-inputValidation-warningBorder);
--color-vscode-widget-border: var(--vscode-widget-border);
--color-vscode-widget-shadow: var(--vscode-widget-shadow);

View file

@ -7,6 +7,7 @@ import {
type RouterModels,
modelIdKeysByProvider,
isProviderName,
isRetiredProvider,
isDynamicProvider,
isFauxProvider,
isCustomProvider,
@ -153,7 +154,8 @@ function validateProviderAgainstOrganizationSettings(
}
if (!providerConfig.allowAll) {
const modelId = getModelIdForProvider(apiConfiguration, provider)
const activeProvider = isRetiredProvider(provider) ? undefined : provider
const modelId = activeProvider ? getModelIdForProvider(apiConfiguration, activeProvider) : undefined
const allowedModels = providerConfig.models || []
if (modelId && !allowedModels.includes(modelId)) {