mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
e8158b4286
commit
aebcaa263e
16 changed files with 731 additions and 355 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
34
webview-ui/src/components/chat/RetiredProviderWarning.tsx
Normal file
34
webview-ui/src/components/chat/RetiredProviderWarning.tsx
Normal 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
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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("")
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue