Просмотр исходного кода

refactor: remove 9 low-usage providers and add retired-provider UX (#11297)

* refactor: remove 9 low-usage providers (Phase 0)

Remove Cerebras, Chutes, DeepInfra, Doubao, Featherless, Groq,
Hugging Face, IO Intelligence, and Unbound providers from the codebase.

Each provider removal includes: handler, tests, model definitions,
type schemas, UI settings components, fetchers, i18n references,
and all wiring in shared registration/config files.

- Delete 42 provider-specific files (handlers, tests, fetchers, UI components)
- Remove @ai-sdk/cerebras and @ai-sdk/groq npm dependencies
- Clean provider references from 68 shared files across src/, packages/types/,
  webview-ui/, and apps/cli/
- Remove ~490 dead i18n translation keys across 36 locale files
- Add docs/ai-sdk-migration-guide.md with updated migration status
- All TypeScript checks pass, 6505 tests pass with 0 failures

* 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

* feat: add retired-provider warning banner in chat view

* Revert "feat: add retired-provider warning banner in chat view"

This reverts commit dd593e10562b587f58acc80e0dec491044e551af.

* feat: show retired-provider message as inline chat response

* fix: show retired provider warning on home screen

Move WarningRow outside {task && ...} conditional so it renders
regardless of task state. Preserve user input on retired provider
intercept so text isn't lost when switching providers.

- Move showRetiredProviderWarning WarningRow to unconditional render
  area near ProfileViolationWarning
- Remove setInputValue/setSelectedImages clearing from retired
  provider early return in handleSendMessage
- Delete unused RetiredProviderWarning.tsx (dead code)

* fix: address PR review — passthrough retired-provider fields and i18n strings

- Use passthrough() in saveConfig() and load() so legacy provider-specific
  fields (e.g. groqApiKey, deepInfraModelId) are preserved instead of
  silently stripped by strict Zod parse()
- Move hardcoded English strings in ApiOptions.tsx and ChatView.tsx to
  i18n translation keys (settings:providers.retiredProviderMessage,
  chat:retiredProvider.{title,message,openSettings})
- Update tests to assert legacy provider-specific fields survive
  save and load round-trips

* i18n: add retired-provider translations for all 17 locales

Translate providers.retiredProviderMessage (settings) and
retiredProvider.{title,message,openSettings} (chat) into ca, de, es,
fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW.

* test: update ApiOptions retired-provider test to expect i18n key
Hannes Rudolph 4 дней назад
Родитель
Сommit
ef2fec9a23
100 измененных файлов с 759 добавлено и 9370 удалено
  1. 1 9
      apps/cli/src/lib/utils/context-window.ts
  2. 1 1
      apps/web-evals/package.json
  3. 0 9
      packages/types/src/global-settings.ts
  4. 30 118
      packages/types/src/provider-settings.ts
  5. 0 58
      packages/types/src/providers/cerebras.ts
  6. 0 421
      packages/types/src/providers/chutes.ts
  7. 0 14
      packages/types/src/providers/deepinfra.ts
  8. 0 44
      packages/types/src/providers/doubao.ts
  9. 0 58
      packages/types/src/providers/featherless.ts
  10. 0 84
      packages/types/src/providers/groq.ts
  11. 0 17
      packages/types/src/providers/huggingface.ts
  12. 0 35
      packages/types/src/providers/index.ts
  13. 0 44
      packages/types/src/providers/io-intelligence.ts
  14. 0 14
      packages/types/src/providers/unbound.ts
  15. 0 19
      packages/types/src/vscode-extension-host.ts
  16. 2 33
      pnpm-lock.yaml
  17. 7 31
      src/api/index.ts
  18. 0 455
      src/api/providers/__tests__/cerebras.spec.ts
  19. 0 490
      src/api/providers/__tests__/chutes.spec.ts
  20. 0 386
      src/api/providers/__tests__/deepinfra.spec.ts
  21. 0 356
      src/api/providers/__tests__/featherless.spec.ts
  22. 0 578
      src/api/providers/__tests__/groq.spec.ts
  23. 0 553
      src/api/providers/__tests__/huggingface.spec.ts
  24. 0 197
      src/api/providers/__tests__/io-intelligence.spec.ts
  25. 0 549
      src/api/providers/__tests__/unbound.spec.ts
  26. 0 169
      src/api/providers/cerebras.ts
  27. 0 242
      src/api/providers/chutes.ts
  28. 0 164
      src/api/providers/deepinfra.ts
  29. 0 87
      src/api/providers/doubao.ts
  30. 0 140
      src/api/providers/featherless.ts
  31. 0 342
      src/api/providers/fetchers/__tests__/chutes.spec.ts
  32. 0 42
      src/api/providers/fetchers/__tests__/modelCache.spec.ts
  33. 0 89
      src/api/providers/fetchers/chutes.ts
  34. 0 71
      src/api/providers/fetchers/deepinfra.ts
  35. 0 252
      src/api/providers/fetchers/huggingface.ts
  36. 0 158
      src/api/providers/fetchers/io-intelligence.ts
  37. 0 22
      src/api/providers/fetchers/modelCache.ts
  38. 0 52
      src/api/providers/fetchers/unbound.ts
  39. 0 181
      src/api/providers/groq.ts
  40. 0 215
      src/api/providers/huggingface.ts
  41. 0 9
      src/api/providers/index.ts
  42. 0 62
      src/api/providers/io-intelligence.ts
  43. 0 208
      src/api/providers/unbound.ts
  44. 3 3
      src/api/transform/__tests__/ai-sdk.spec.ts
  45. 14 7
      src/core/config/ContextProxy.ts
  46. 42 8
      src/core/config/ProviderSettingsManager.ts
  47. 52 6
      src/core/config/__tests__/ContextProxy.spec.ts
  48. 124 9
      src/core/config/__tests__/ProviderSettingsManager.spec.ts
  49. 0 38
      src/core/context/context-management/__tests__/context-error-handling.test.ts
  50. 1 20
      src/core/context/context-management/context-error-handling.ts
  51. 21 4
      src/core/task/Task.ts
  52. 9 3
      src/core/webview/ClineProvider.ts
  53. 0 46
      src/core/webview/__tests__/ClineProvider.spec.ts
  54. 0 6
      src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts
  55. 0 63
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  56. 0 41
      src/core/webview/webviewMessageHandler.ts
  57. 1 13
      src/i18n/locales/ca/common.json
  58. 1 13
      src/i18n/locales/de/common.json
  59. 0 9
      src/i18n/locales/en/common.json
  60. 1 13
      src/i18n/locales/es/common.json
  61. 1 13
      src/i18n/locales/fr/common.json
  62. 1 13
      src/i18n/locales/hi/common.json
  63. 1 13
      src/i18n/locales/id/common.json
  64. 1 13
      src/i18n/locales/it/common.json
  65. 1 13
      src/i18n/locales/ja/common.json
  66. 1 13
      src/i18n/locales/ko/common.json
  67. 1 13
      src/i18n/locales/nl/common.json
  68. 1 13
      src/i18n/locales/pl/common.json
  69. 1 13
      src/i18n/locales/pt-BR/common.json
  70. 1 13
      src/i18n/locales/ru/common.json
  71. 1 13
      src/i18n/locales/tr/common.json
  72. 1 13
      src/i18n/locales/vi/common.json
  73. 1 13
      src/i18n/locales/zh-CN/common.json
  74. 1 13
      src/i18n/locales/zh-TW/common.json
  75. 0 2
      src/package.json
  76. 0 9
      src/shared/ProfileValidator.ts
  77. 0 34
      src/shared/__tests__/ProfileValidator.spec.ts
  78. 0 1
      src/shared/__tests__/checkExistApiConfig.spec.ts
  79. 0 5
      src/shared/api.ts
  80. 40 2
      webview-ui/src/components/chat/ChatView.tsx
  81. 335 375
      webview-ui/src/components/settings/ApiOptions.tsx
  82. 8 6
      webview-ui/src/components/settings/ModelPicker.tsx
  83. 0 2
      webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx
  84. 27 0
      webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx
  85. 0 17
      webview-ui/src/components/settings/constants.ts
  86. 0 50
      webview-ui/src/components/settings/providers/Cerebras.tsx
  87. 0 76
      webview-ui/src/components/settings/providers/Chutes.tsx
  88. 0 100
      webview-ui/src/components/settings/providers/DeepInfra.tsx
  89. 0 53
      webview-ui/src/components/settings/providers/Doubao.tsx
  90. 0 50
      webview-ui/src/components/settings/providers/Featherless.tsx
  91. 0 50
      webview-ui/src/components/settings/providers/Groq.tsx
  92. 0 277
      webview-ui/src/components/settings/providers/HuggingFace.tsx
  93. 0 80
      webview-ui/src/components/settings/providers/IOIntelligence.tsx
  94. 0 197
      webview-ui/src/components/settings/providers/Unbound.tsx
  95. 0 300
      webview-ui/src/components/settings/providers/__tests__/HuggingFace.spec.tsx
  96. 0 9
      webview-ui/src/components/settings/providers/index.ts
  97. 0 17
      webview-ui/src/components/settings/utils/providerModelConfig.ts
  98. 1 21
      webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts
  99. 19 75
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  100. 5 0
      webview-ui/src/i18n/locales/ca/chat.json

+ 1 - 9
apps/cli/src/lib/utils/context-window.ts

@@ -48,18 +48,10 @@ function getModelIdForProvider(config: ProviderSettings): string | undefined {
 			return config.requestyModelId
 		case "litellm":
 			return config.litellmModelId
-		case "deepinfra":
-			return config.deepInfraModelId
-		case "huggingface":
-			return config.huggingFaceModelId
-		case "unbound":
-			return config.unboundModelId
 		case "vercel-ai-gateway":
 			return config.vercelAiGatewayModelId
-		case "io-intelligence":
-			return config.ioIntelligenceModelId
 		default:
-			// For anthropic, bedrock, vertex, gemini, xai, groq, etc.
+			// For anthropic, bedrock, vertex, gemini, xai, etc.
 			return config.apiModelId
 	}
 }

+ 1 - 1
apps/web-evals/package.json

@@ -27,7 +27,7 @@
 		"@radix-ui/react-tabs": "^1.1.3",
 		"@radix-ui/react-tooltip": "^1.2.8",
 		"@roo-code/evals": "workspace:^",
-		"@roo-code/types": "^1.108.0",
+		"@roo-code/types": "workspace:^",
 		"@tanstack/react-query": "^5.69.0",
 		"archiver": "^7.0.1",
 		"class-variance-authority": "^0.7.1",

+ 0 - 9
packages/types/src/global-settings.ts

@@ -268,19 +268,13 @@ export const SECRET_STATE_KEYS = [
 	"ollamaApiKey",
 	"geminiApiKey",
 	"openAiNativeApiKey",
-	"cerebrasApiKey",
 	"deepSeekApiKey",
-	"doubaoApiKey",
 	"moonshotApiKey",
 	"mistralApiKey",
 	"minimaxApiKey",
-	"unboundApiKey",
 	"requestyApiKey",
 	"xaiApiKey",
-	"groqApiKey",
-	"chutesApiKey",
 	"litellmApiKey",
-	"deepInfraApiKey",
 	"codeIndexOpenAiKey",
 	"codeIndexQdrantApiKey",
 	"codebaseIndexOpenAiCompatibleApiKey",
@@ -288,12 +282,9 @@ export const SECRET_STATE_KEYS = [
 	"codebaseIndexMistralApiKey",
 	"codebaseIndexVercelAiGatewayApiKey",
 	"codebaseIndexOpenRouterApiKey",
-	"huggingFaceApiKey",
 	"sambaNovaApiKey",
 	"zaiApiKey",
 	"fireworksApiKey",
-	"featherlessApiKey",
-	"ioIntelligenceApiKey",
 	"vercelAiGatewayApiKey",
 	"basetenApiKey",
 ] as const

+ 30 - 118
packages/types/src/provider-settings.ts

@@ -6,14 +6,9 @@ import {
 	anthropicModels,
 	basetenModels,
 	bedrockModels,
-	cerebrasModels,
 	deepSeekModels,
-	doubaoModels,
-	featherlessModels,
 	fireworksModels,
 	geminiModels,
-	groqModels,
-	ioIntelligenceModels,
 	mistralModels,
 	moonshotModels,
 	openAiCodexModels,
@@ -39,18 +34,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
  * Dynamic provider requires external API calls in order to get the model list.
  */
 
-export const dynamicProviders = [
-	"openrouter",
-	"vercel-ai-gateway",
-	"huggingface",
-	"litellm",
-	"deepinfra",
-	"io-intelligence",
-	"requesty",
-	"unbound",
-	"roo",
-	"chutes",
-] as const
+export const dynamicProviders = ["openrouter", "vercel-ai-gateway", "litellm", "requesty", "roo"] as const
 
 export type DynamicProvider = (typeof dynamicProviders)[number]
 
@@ -121,14 +105,10 @@ export const providerNames = [
 	"anthropic",
 	"bedrock",
 	"baseten",
-	"cerebras",
-	"doubao",
 	"deepseek",
-	"featherless",
 	"fireworks",
 	"gemini",
 	"gemini-cli",
-	"groq",
 	"mistral",
 	"moonshot",
 	"minimax",
@@ -149,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
  */
@@ -156,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(),
 })
 
@@ -300,17 +307,6 @@ const deepSeekSchema = apiModelIdProviderModelSchema.extend({
 	deepSeekApiKey: z.string().optional(),
 })
 
-const deepInfraSchema = apiModelIdProviderModelSchema.extend({
-	deepInfraBaseUrl: z.string().optional(),
-	deepInfraApiKey: z.string().optional(),
-	deepInfraModelId: z.string().optional(),
-})
-
-const doubaoSchema = apiModelIdProviderModelSchema.extend({
-	doubaoBaseUrl: z.string().optional(),
-	doubaoApiKey: z.string().optional(),
-})
-
 const moonshotSchema = apiModelIdProviderModelSchema.extend({
 	moonshotBaseUrl: z
 		.union([z.literal("https://api.moonshot.ai/v1"), z.literal("https://api.moonshot.cn/v1")])
@@ -325,11 +321,6 @@ const minimaxSchema = apiModelIdProviderModelSchema.extend({
 	minimaxApiKey: z.string().optional(),
 })
 
-const unboundSchema = baseProviderSettingsSchema.extend({
-	unboundApiKey: z.string().optional(),
-	unboundModelId: z.string().optional(),
-})
-
 const requestySchema = baseProviderSettingsSchema.extend({
 	requestyBaseUrl: z.string().optional(),
 	requestyApiKey: z.string().optional(),
@@ -344,20 +335,6 @@ const xaiSchema = apiModelIdProviderModelSchema.extend({
 	xaiApiKey: z.string().optional(),
 })
 
-const groqSchema = apiModelIdProviderModelSchema.extend({
-	groqApiKey: z.string().optional(),
-})
-
-const huggingFaceSchema = baseProviderSettingsSchema.extend({
-	huggingFaceApiKey: z.string().optional(),
-	huggingFaceModelId: z.string().optional(),
-	huggingFaceInferenceProvider: z.string().optional(),
-})
-
-const chutesSchema = apiModelIdProviderModelSchema.extend({
-	chutesApiKey: z.string().optional(),
-})
-
 const litellmSchema = baseProviderSettingsSchema.extend({
 	litellmBaseUrl: z.string().optional(),
 	litellmApiKey: z.string().optional(),
@@ -365,10 +342,6 @@ const litellmSchema = baseProviderSettingsSchema.extend({
 	litellmUsePromptCache: z.boolean().optional(),
 })
 
-const cerebrasSchema = apiModelIdProviderModelSchema.extend({
-	cerebrasApiKey: z.string().optional(),
-})
-
 const sambaNovaSchema = apiModelIdProviderModelSchema.extend({
 	sambaNovaApiKey: z.string().optional(),
 })
@@ -386,15 +359,6 @@ const fireworksSchema = apiModelIdProviderModelSchema.extend({
 	fireworksApiKey: z.string().optional(),
 })
 
-const featherlessSchema = apiModelIdProviderModelSchema.extend({
-	featherlessApiKey: z.string().optional(),
-})
-
-const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
-	ioIntelligenceModelId: z.string().optional(),
-	ioIntelligenceApiKey: z.string().optional(),
-})
-
 const qwenCodeSchema = apiModelIdProviderModelSchema.extend({
 	qwenCodeOauthPath: z.string().optional(),
 })
@@ -432,25 +396,16 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })),
 	mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })),
 	deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })),
-	deepInfraSchema.merge(z.object({ apiProvider: z.literal("deepinfra") })),
-	doubaoSchema.merge(z.object({ apiProvider: z.literal("doubao") })),
 	moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })),
 	minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })),
-	unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })),
 	requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })),
 	fakeAiSchema.merge(z.object({ apiProvider: z.literal("fake-ai") })),
 	xaiSchema.merge(z.object({ apiProvider: z.literal("xai") })),
-	groqSchema.merge(z.object({ apiProvider: z.literal("groq") })),
 	basetenSchema.merge(z.object({ apiProvider: z.literal("baseten") })),
-	huggingFaceSchema.merge(z.object({ apiProvider: z.literal("huggingface") })),
-	chutesSchema.merge(z.object({ apiProvider: z.literal("chutes") })),
 	litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })),
-	cerebrasSchema.merge(z.object({ apiProvider: z.literal("cerebras") })),
 	sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })),
 	zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })),
 	fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
-	featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })),
-	ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
 	qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
 	rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
 	vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
@@ -458,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,
@@ -473,25 +428,16 @@ export const providerSettingsSchema = z.object({
 	...openAiNativeSchema.shape,
 	...mistralSchema.shape,
 	...deepSeekSchema.shape,
-	...deepInfraSchema.shape,
-	...doubaoSchema.shape,
 	...moonshotSchema.shape,
 	...minimaxSchema.shape,
-	...unboundSchema.shape,
 	...requestySchema.shape,
 	...fakeAiSchema.shape,
 	...xaiSchema.shape,
-	...groqSchema.shape,
 	...basetenSchema.shape,
-	...huggingFaceSchema.shape,
-	...chutesSchema.shape,
 	...litellmSchema.shape,
-	...cerebrasSchema.shape,
 	...sambaNovaSchema.shape,
 	...zaiSchema.shape,
 	...fireworksSchema.shape,
-	...featherlessSchema.shape,
-	...ioIntelligenceSchema.shape,
 	...qwenCodeSchema.shape,
 	...rooSchema.shape,
 	...vercelAiGatewaySchema.shape,
@@ -521,13 +467,9 @@ export const modelIdKeys = [
 	"ollamaModelId",
 	"lmStudioModelId",
 	"lmStudioDraftModelId",
-	"unboundModelId",
 	"requestyModelId",
 	"litellmModelId",
-	"huggingFaceModelId",
-	"ioIntelligenceModelId",
 	"vercelAiGatewayModelId",
-	"deepInfraModelId",
 ] as const satisfies readonly (keyof ProviderSettings)[]
 
 export type ModelIdKey = (typeof modelIdKeys)[number]
@@ -561,23 +503,14 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
 	moonshot: "apiModelId",
 	minimax: "apiModelId",
 	deepseek: "apiModelId",
-	deepinfra: "deepInfraModelId",
-	doubao: "apiModelId",
 	"qwen-code": "apiModelId",
-	unbound: "unboundModelId",
 	requesty: "requestyModelId",
 	xai: "apiModelId",
-	groq: "apiModelId",
 	baseten: "apiModelId",
-	chutes: "apiModelId",
 	litellm: "litellmModelId",
-	huggingface: "huggingFaceModelId",
-	cerebras: "apiModelId",
 	sambanova: "apiModelId",
 	zai: "apiModelId",
 	fireworks: "apiModelId",
-	featherless: "apiModelId",
-	"io-intelligence": "ioIntelligenceModelId",
 	roo: "apiModelId",
 	"vercel-ai-gateway": "vercelAiGatewayModelId",
 }
@@ -629,22 +562,11 @@ export const MODELS_BY_PROVIDER: Record<
 		label: "Amazon Bedrock",
 		models: Object.keys(bedrockModels),
 	},
-	cerebras: {
-		id: "cerebras",
-		label: "Cerebras",
-		models: Object.keys(cerebrasModels),
-	},
 	deepseek: {
 		id: "deepseek",
 		label: "DeepSeek",
 		models: Object.keys(deepSeekModels),
 	},
-	doubao: { id: "doubao", label: "Doubao", models: Object.keys(doubaoModels) },
-	featherless: {
-		id: "featherless",
-		label: "Featherless",
-		models: Object.keys(featherlessModels),
-	},
 	fireworks: {
 		id: "fireworks",
 		label: "Fireworks",
@@ -655,12 +577,6 @@ export const MODELS_BY_PROVIDER: Record<
 		label: "Google Gemini",
 		models: Object.keys(geminiModels),
 	},
-	groq: { id: "groq", label: "Groq", models: Object.keys(groqModels) },
-	"io-intelligence": {
-		id: "io-intelligence",
-		label: "IO Intelligence",
-		models: Object.keys(ioIntelligenceModels),
-	},
 	mistral: {
 		id: "mistral",
 		label: "Mistral",
@@ -708,14 +624,10 @@ export const MODELS_BY_PROVIDER: Record<
 	baseten: { id: "baseten", label: "Baseten", models: Object.keys(basetenModels) },
 
 	// Dynamic providers; models pulled from remote APIs.
-	huggingface: { id: "huggingface", label: "Hugging Face", models: [] },
 	litellm: { id: "litellm", label: "LiteLLM", models: [] },
 	openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
 	requesty: { id: "requesty", label: "Requesty", models: [] },
-	unbound: { id: "unbound", label: "Unbound", models: [] },
-	deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
 	"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
-	chutes: { id: "chutes", label: "Chutes AI", models: [] },
 
 	// Local providers; models discovered from localhost endpoints.
 	lmstudio: { id: "lmstudio", label: "LM Studio", models: [] },

+ 0 - 58
packages/types/src/providers/cerebras.ts

@@ -1,58 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-// https://inference-docs.cerebras.ai/api-reference/chat-completions
-export type CerebrasModelId = keyof typeof cerebrasModels
-
-export const cerebrasDefaultModelId: CerebrasModelId = "gpt-oss-120b"
-
-export const cerebrasModels = {
-	"zai-glm-4.7": {
-		maxTokens: 16384, // Conservative default to avoid premature rate limiting (Cerebras reserves quota upfront)
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: true,
-		supportsTemperature: true,
-		defaultTemperature: 1.0,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.",
-	},
-	"qwen-3-235b-a22b-instruct-2507": {
-		maxTokens: 16384, // Conservative default to avoid premature rate limiting
-		contextWindow: 64000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Intelligent model with ~1400 tokens/s",
-	},
-	"llama-3.3-70b": {
-		maxTokens: 16384, // Conservative default to avoid premature rate limiting
-		contextWindow: 64000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Powerful model with ~2600 tokens/s",
-	},
-	"qwen-3-32b": {
-		maxTokens: 16384, // Conservative default to avoid premature rate limiting
-		contextWindow: 64000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "SOTA coding performance with ~2500 tokens/s",
-	},
-	"gpt-oss-120b": {
-		maxTokens: 16384, // Conservative default to avoid premature rate limiting
-		contextWindow: 64000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"OpenAI GPT OSS model with ~2800 tokens/s\n\n• 64K context window\n• Excels at efficient reasoning across science, math, and coding",
-	},
-} as const satisfies Record<string, ModelInfo>

+ 0 - 421
packages/types/src/providers/chutes.ts

@@ -1,421 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-// https://llm.chutes.ai/v1 (OpenAI compatible)
-export type ChutesModelId =
-	| "deepseek-ai/DeepSeek-R1-0528"
-	| "deepseek-ai/DeepSeek-R1"
-	| "deepseek-ai/DeepSeek-V3"
-	| "deepseek-ai/DeepSeek-V3.1"
-	| "deepseek-ai/DeepSeek-V3.1-Terminus"
-	| "deepseek-ai/DeepSeek-V3.1-turbo"
-	| "deepseek-ai/DeepSeek-V3.2-Exp"
-	| "unsloth/Llama-3.3-70B-Instruct"
-	| "chutesai/Llama-4-Scout-17B-16E-Instruct"
-	| "unsloth/Mistral-Nemo-Instruct-2407"
-	| "unsloth/gemma-3-12b-it"
-	| "NousResearch/DeepHermes-3-Llama-3-8B-Preview"
-	| "unsloth/gemma-3-4b-it"
-	| "nvidia/Llama-3_3-Nemotron-Super-49B-v1"
-	| "nvidia/Llama-3_1-Nemotron-Ultra-253B-v1"
-	| "chutesai/Llama-4-Maverick-17B-128E-Instruct-FP8"
-	| "deepseek-ai/DeepSeek-V3-Base"
-	| "deepseek-ai/DeepSeek-R1-Zero"
-	| "deepseek-ai/DeepSeek-V3-0324"
-	| "Qwen/Qwen3-235B-A22B"
-	| "Qwen/Qwen3-235B-A22B-Instruct-2507"
-	| "Qwen/Qwen3-32B"
-	| "Qwen/Qwen3-30B-A3B"
-	| "Qwen/Qwen3-14B"
-	| "Qwen/Qwen3-8B"
-	| "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8"
-	| "microsoft/MAI-DS-R1-FP8"
-	| "tngtech/DeepSeek-R1T-Chimera"
-	| "zai-org/GLM-4.5-Air"
-	| "zai-org/GLM-4.5-FP8"
-	| "zai-org/GLM-4.5-turbo"
-	| "zai-org/GLM-4.6-FP8"
-	| "zai-org/GLM-4.6-turbo"
-	| "meituan-longcat/LongCat-Flash-Thinking-FP8"
-	| "moonshotai/Kimi-K2-Instruct-75k"
-	| "moonshotai/Kimi-K2-Instruct-0905"
-	| "Qwen/Qwen3-235B-A22B-Thinking-2507"
-	| "Qwen/Qwen3-Next-80B-A3B-Instruct"
-	| "Qwen/Qwen3-Next-80B-A3B-Thinking"
-	| "Qwen/Qwen3-VL-235B-A22B-Thinking"
-
-export const chutesDefaultModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1-0528"
-
-export const chutesModels = {
-	"deepseek-ai/DeepSeek-R1-0528": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek R1 0528 model.",
-	},
-	"deepseek-ai/DeepSeek-R1": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek R1 model.",
-	},
-	"deepseek-ai/DeepSeek-V3": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek V3 model.",
-	},
-	"deepseek-ai/DeepSeek-V3.1": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek V3.1 model.",
-	},
-	"deepseek-ai/DeepSeek-V3.1-Terminus": {
-		maxTokens: 163840,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.23,
-		outputPrice: 0.9,
-		description:
-			"DeepSeek‑V3.1‑Terminus is an update to V3.1 that improves language consistency by reducing CN/EN mix‑ups and eliminating random characters, while strengthening agent capabilities with notably better Code Agent and Search Agent performance.",
-	},
-	"deepseek-ai/DeepSeek-V3.1-turbo": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 1.0,
-		outputPrice: 3.0,
-		description:
-			"DeepSeek-V3.1-turbo is an FP8, speculative-decoding turbo variant optimized for ultra-fast single-shot queries (~200 TPS), with outputs close to the originals and solid function calling/reasoning/structured output, priced at $1/M input and $3/M output tokens, using 2× quota per request and not intended for bulk workloads.",
-	},
-	"deepseek-ai/DeepSeek-V3.2-Exp": {
-		maxTokens: 163840,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.25,
-		outputPrice: 0.35,
-		description:
-			"DeepSeek-V3.2-Exp is an experimental LLM that introduces DeepSeek Sparse Attention to improve long‑context training and inference efficiency while maintaining performance comparable to V3.1‑Terminus.",
-	},
-	"unsloth/Llama-3.3-70B-Instruct": {
-		maxTokens: 32768, // From Groq
-		contextWindow: 131072, // From Groq
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Unsloth Llama 3.3 70B Instruct model.",
-	},
-	"chutesai/Llama-4-Scout-17B-16E-Instruct": {
-		maxTokens: 32768,
-		contextWindow: 512000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "ChutesAI Llama 4 Scout 17B Instruct model, 512K context.",
-	},
-	"unsloth/Mistral-Nemo-Instruct-2407": {
-		maxTokens: 32768,
-		contextWindow: 128000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Unsloth Mistral Nemo Instruct model.",
-	},
-	"unsloth/gemma-3-12b-it": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Unsloth Gemma 3 12B IT model.",
-	},
-	"NousResearch/DeepHermes-3-Llama-3-8B-Preview": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Nous DeepHermes 3 Llama 3 8B Preview model.",
-	},
-	"unsloth/gemma-3-4b-it": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Unsloth Gemma 3 4B IT model.",
-	},
-	"nvidia/Llama-3_3-Nemotron-Super-49B-v1": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Nvidia Llama 3.3 Nemotron Super 49B model.",
-	},
-	"nvidia/Llama-3_1-Nemotron-Ultra-253B-v1": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Nvidia Llama 3.1 Nemotron Ultra 253B model.",
-	},
-	"chutesai/Llama-4-Maverick-17B-128E-Instruct-FP8": {
-		maxTokens: 32768,
-		contextWindow: 256000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "ChutesAI Llama 4 Maverick 17B Instruct FP8 model.",
-	},
-	"deepseek-ai/DeepSeek-V3-Base": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek V3 Base model.",
-	},
-	"deepseek-ai/DeepSeek-R1-Zero": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek R1 Zero model.",
-	},
-	"deepseek-ai/DeepSeek-V3-0324": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek V3 (0324) model.",
-	},
-	"Qwen/Qwen3-235B-A22B-Instruct-2507": {
-		maxTokens: 32768,
-		contextWindow: 262144,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 235B A22B Instruct 2507 model with 262K context window.",
-	},
-	"Qwen/Qwen3-235B-A22B": {
-		maxTokens: 32768,
-		contextWindow: 40960,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 235B A22B model.",
-	},
-	"Qwen/Qwen3-32B": {
-		maxTokens: 32768,
-		contextWindow: 40960,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 32B model.",
-	},
-	"Qwen/Qwen3-30B-A3B": {
-		maxTokens: 32768,
-		contextWindow: 40960,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 30B A3B model.",
-	},
-	"Qwen/Qwen3-14B": {
-		maxTokens: 32768,
-		contextWindow: 40960,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 14B model.",
-	},
-	"Qwen/Qwen3-8B": {
-		maxTokens: 32768,
-		contextWindow: 40960,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 8B model.",
-	},
-	"microsoft/MAI-DS-R1-FP8": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Microsoft MAI-DS-R1 FP8 model.",
-	},
-	"tngtech/DeepSeek-R1T-Chimera": {
-		maxTokens: 32768,
-		contextWindow: 163840,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "TNGTech DeepSeek R1T Chimera model.",
-	},
-	"zai-org/GLM-4.5-Air": {
-		maxTokens: 32768,
-		contextWindow: 151329,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"GLM-4.5-Air model with 151,329 token context window and 106B total parameters with 12B activated.",
-	},
-	"zai-org/GLM-4.5-FP8": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"GLM-4.5-FP8 model with 128k token context window, optimized for agent-based applications with MoE architecture.",
-	},
-	"zai-org/GLM-4.5-turbo": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 1,
-		outputPrice: 3,
-		description: "GLM-4.5-turbo model with 128K token context window, optimized for fast inference.",
-	},
-	"zai-org/GLM-4.6-FP8": {
-		maxTokens: 32768,
-		contextWindow: 202752,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"GLM-4.6 introduces major upgrades over GLM-4.5, including a longer 200K-token context window for complex tasks, stronger coding performance in benchmarks and real-world tools (such as Claude Code, Cline, Roo Code, and Kilo Code), improved reasoning with tool use during inference, more capable and efficient agent integration, and refined writing that better matches human style, readability, and natural role-play scenarios.",
-	},
-	"zai-org/GLM-4.6-turbo": {
-		maxTokens: 202752, // From Chutes /v1/models: max_output_length
-		contextWindow: 202752,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 1.15,
-		outputPrice: 3.25,
-		description: "GLM-4.6-turbo model with 200K-token context window, optimized for fast inference.",
-	},
-	"meituan-longcat/LongCat-Flash-Thinking-FP8": {
-		maxTokens: 32768,
-		contextWindow: 128000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"LongCat Flash Thinking FP8 model with 128K context window, optimized for complex reasoning and coding tasks.",
-	},
-	"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": {
-		maxTokens: 32768,
-		contextWindow: 262144,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 Coder 480B A35B Instruct FP8 model, optimized for coding tasks.",
-	},
-	"moonshotai/Kimi-K2-Instruct-75k": {
-		maxTokens: 32768,
-		contextWindow: 75000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.1481,
-		outputPrice: 0.5926,
-		description: "Moonshot AI Kimi K2 Instruct model with 75k context window.",
-	},
-	"moonshotai/Kimi-K2-Instruct-0905": {
-		maxTokens: 32768,
-		contextWindow: 262144,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.1999,
-		outputPrice: 0.8001,
-		description: "Moonshot AI Kimi K2 Instruct 0905 model with 256k context window.",
-	},
-	"Qwen/Qwen3-235B-A22B-Thinking-2507": {
-		maxTokens: 32768,
-		contextWindow: 262144,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.077968332,
-		outputPrice: 0.31202496,
-		description: "Qwen3 235B A22B Thinking 2507 model with 262K context window.",
-	},
-	"Qwen/Qwen3-Next-80B-A3B-Instruct": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"Fast, stable instruction-tuned model optimized for complex tasks, RAG, and tool use without thinking traces.",
-	},
-	"Qwen/Qwen3-Next-80B-A3B-Thinking": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description:
-			"Reasoning-first model with structured thinking traces for multi-step problems, math proofs, and code synthesis.",
-	},
-	"Qwen/Qwen3-VL-235B-A22B-Thinking": {
-		maxTokens: 262144,
-		contextWindow: 262144,
-		supportsImages: true,
-		supportsPromptCache: false,
-		inputPrice: 0.16,
-		outputPrice: 0.65,
-		description:
-			"Qwen3‑VL‑235B‑A22B‑Thinking is an open‑weight MoE vision‑language model (235B total, ~22B activated) optimized for deliberate multi‑step reasoning with strong text‑image‑video understanding and long‑context capabilities.",
-	},
-} as const satisfies Record<string, ModelInfo>
-
-export const chutesDefaultModelInfo: ModelInfo = chutesModels[chutesDefaultModelId]

+ 0 - 14
packages/types/src/providers/deepinfra.ts

@@ -1,14 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-// Default fallback values for DeepInfra when model metadata is not yet loaded.
-export const deepInfraDefaultModelId = "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo"
-
-export const deepInfraDefaultModelInfo: ModelInfo = {
-	maxTokens: 16384,
-	contextWindow: 262144,
-	supportsImages: false,
-	supportsPromptCache: false,
-	inputPrice: 0.3,
-	outputPrice: 1.2,
-	description: "Qwen 3 Coder 480B A35B Instruct Turbo model, 256K context.",
-}

+ 0 - 44
packages/types/src/providers/doubao.ts

@@ -1,44 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-export const doubaoDefaultModelId = "doubao-seed-1-6-250615"
-
-export const doubaoModels = {
-	"doubao-seed-1-6-250615": {
-		maxTokens: 32_768,
-		contextWindow: 128_000,
-		supportsImages: true,
-		supportsPromptCache: true,
-		inputPrice: 0.0001, // $0.0001 per million tokens (cache miss)
-		outputPrice: 0.0004, // $0.0004 per million tokens
-		cacheWritesPrice: 0.0001, // $0.0001 per million tokens (cache miss)
-		cacheReadsPrice: 0.00002, // $0.00002 per million tokens (cache hit)
-		description: `Doubao Seed 1.6 is a powerful model designed for high-performance tasks with extensive context handling.`,
-	},
-	"doubao-seed-1-6-thinking-250715": {
-		maxTokens: 32_768,
-		contextWindow: 128_000,
-		supportsImages: true,
-		supportsPromptCache: true,
-		inputPrice: 0.0002, // $0.0002 per million tokens
-		outputPrice: 0.0008, // $0.0008 per million tokens
-		cacheWritesPrice: 0.0002, // $0.0002 per million
-		cacheReadsPrice: 0.00004, // $0.00004 per million tokens (cache hit)
-		description: `Doubao Seed 1.6 Thinking is optimized for reasoning tasks, providing enhanced performance in complex problem-solving scenarios.`,
-	},
-	"doubao-seed-1-6-flash-250715": {
-		maxTokens: 32_768,
-		contextWindow: 128_000,
-		supportsImages: true,
-		supportsPromptCache: true,
-		inputPrice: 0.00015, // $0.00015 per million tokens
-		outputPrice: 0.0006, // $0.0006 per million tokens
-		cacheWritesPrice: 0.00015, // $0.00015 per million
-		cacheReadsPrice: 0.00003, // $0.00003 per million tokens (cache hit)
-		description: `Doubao Seed 1.6 Flash is tailored for speed and efficiency, making it ideal for applications requiring rapid responses.`,
-	},
-} as const satisfies Record<string, ModelInfo>
-
-export const doubaoDefaultModelInfo: ModelInfo = doubaoModels[doubaoDefaultModelId]
-
-export const DOUBAO_API_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
-export const DOUBAO_API_CHAT_PATH = "/chat/completions"

+ 0 - 58
packages/types/src/providers/featherless.ts

@@ -1,58 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-export type FeatherlessModelId =
-	| "deepseek-ai/DeepSeek-V3-0324"
-	| "deepseek-ai/DeepSeek-R1-0528"
-	| "moonshotai/Kimi-K2-Instruct"
-	| "openai/gpt-oss-120b"
-	| "Qwen/Qwen3-Coder-480B-A35B-Instruct"
-
-export const featherlessModels = {
-	"deepseek-ai/DeepSeek-V3-0324": {
-		maxTokens: 4096,
-		contextWindow: 32678,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek V3 0324 model.",
-	},
-	"deepseek-ai/DeepSeek-R1-0528": {
-		maxTokens: 4096,
-		contextWindow: 32678,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "DeepSeek R1 0528 model.",
-	},
-	"moonshotai/Kimi-K2-Instruct": {
-		maxTokens: 4096,
-		contextWindow: 32678,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Kimi K2 Instruct model.",
-	},
-	"openai/gpt-oss-120b": {
-		maxTokens: 4096,
-		contextWindow: 32678,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "GPT-OSS 120B model.",
-	},
-	"Qwen/Qwen3-Coder-480B-A35B-Instruct": {
-		maxTokens: 4096,
-		contextWindow: 32678,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 Coder 480B A35B Instruct model.",
-	},
-} as const satisfies Record<string, ModelInfo>
-
-export const featherlessDefaultModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"

+ 0 - 84
packages/types/src/providers/groq.ts

@@ -1,84 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-// https://console.groq.com/docs/models
-export type GroqModelId =
-	| "llama-3.1-8b-instant"
-	| "llama-3.3-70b-versatile"
-	| "meta-llama/llama-4-scout-17b-16e-instruct"
-	| "qwen/qwen3-32b"
-	| "moonshotai/kimi-k2-instruct-0905"
-	| "openai/gpt-oss-120b"
-	| "openai/gpt-oss-20b"
-
-export const groqDefaultModelId: GroqModelId = "moonshotai/kimi-k2-instruct-0905"
-
-export const groqModels = {
-	// Models based on API response: https://api.groq.com/openai/v1/models
-	"llama-3.1-8b-instant": {
-		maxTokens: 8192,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.05,
-		outputPrice: 0.08,
-		description: "Meta Llama 3.1 8B Instant model, 128K context.",
-	},
-	"llama-3.3-70b-versatile": {
-		maxTokens: 8192,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.59,
-		outputPrice: 0.79,
-		description: "Meta Llama 3.3 70B Versatile model, 128K context.",
-	},
-	"meta-llama/llama-4-scout-17b-16e-instruct": {
-		maxTokens: 8192,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.11,
-		outputPrice: 0.34,
-		description: "Meta Llama 4 Scout 17B Instruct model, 128K context.",
-	},
-	"qwen/qwen3-32b": {
-		maxTokens: 8192,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.29,
-		outputPrice: 0.59,
-		description: "Alibaba Qwen 3 32B model, 128K context.",
-	},
-	"moonshotai/kimi-k2-instruct-0905": {
-		maxTokens: 16384,
-		contextWindow: 262144,
-		supportsImages: false,
-		supportsPromptCache: true,
-		inputPrice: 0.6,
-		outputPrice: 2.5,
-		cacheReadsPrice: 0.15,
-		description:
-			"Kimi K2 model gets a new version update: Agentic coding: more accurate, better generalization across scaffolds. Frontend coding: improved aesthetics and functionalities on web, 3d, and other tasks. Context length: extended from 128k to 256k, providing better long-horizon support.",
-	},
-	"openai/gpt-oss-120b": {
-		maxTokens: 32766,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.15,
-		outputPrice: 0.75,
-		description:
-			"GPT-OSS 120B is OpenAI's flagship open source model, built on a Mixture-of-Experts (MoE) architecture with 20 billion parameters and 128 experts.",
-	},
-	"openai/gpt-oss-20b": {
-		maxTokens: 32768,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0.1,
-		outputPrice: 0.5,
-		description:
-			"GPT-OSS 20B is OpenAI's flagship open source model, built on a Mixture-of-Experts (MoE) architecture with 20 billion parameters and 32 experts.",
-	},
-} as const satisfies Record<string, ModelInfo>

+ 0 - 17
packages/types/src/providers/huggingface.ts

@@ -1,17 +0,0 @@
-/**
- * HuggingFace provider constants
- */
-
-// Default values for HuggingFace models
-export const HUGGINGFACE_DEFAULT_MAX_TOKENS = 2048
-export const HUGGINGFACE_MAX_TOKENS_FALLBACK = 8192
-export const HUGGINGFACE_DEFAULT_CONTEXT_WINDOW = 128_000
-
-// UI constants
-export const HUGGINGFACE_SLIDER_STEP = 256
-export const HUGGINGFACE_SLIDER_MIN = 1
-export const HUGGINGFACE_TEMPERATURE_MAX_VALUE = 2
-
-// API constants
-export const HUGGINGFACE_API_URL = "https://router.huggingface.co/v1/models?collection=roocode"
-export const HUGGINGFACE_CACHE_DURATION = 1000 * 60 * 60 // 1 hour

+ 0 - 35
packages/types/src/providers/index.ts

@@ -1,16 +1,9 @@
 export * from "./anthropic.js"
 export * from "./baseten.js"
 export * from "./bedrock.js"
-export * from "./cerebras.js"
-export * from "./chutes.js"
 export * from "./deepseek.js"
-export * from "./doubao.js"
-export * from "./featherless.js"
 export * from "./fireworks.js"
 export * from "./gemini.js"
-export * from "./groq.js"
-export * from "./huggingface.js"
-export * from "./io-intelligence.js"
 export * from "./lite-llm.js"
 export * from "./lm-studio.js"
 export * from "./mistral.js"
@@ -24,27 +17,19 @@ export * from "./qwen-code.js"
 export * from "./requesty.js"
 export * from "./roo.js"
 export * from "./sambanova.js"
-export * from "./unbound.js"
 export * from "./vertex.js"
 export * from "./vscode-llm.js"
 export * from "./xai.js"
 export * from "./vercel-ai-gateway.js"
 export * from "./zai.js"
-export * from "./deepinfra.js"
 export * from "./minimax.js"
 
 import { anthropicDefaultModelId } from "./anthropic.js"
 import { basetenDefaultModelId } from "./baseten.js"
 import { bedrockDefaultModelId } from "./bedrock.js"
-import { cerebrasDefaultModelId } from "./cerebras.js"
-import { chutesDefaultModelId } from "./chutes.js"
 import { deepSeekDefaultModelId } from "./deepseek.js"
-import { doubaoDefaultModelId } from "./doubao.js"
-import { featherlessDefaultModelId } from "./featherless.js"
 import { fireworksDefaultModelId } from "./fireworks.js"
 import { geminiDefaultModelId } from "./gemini.js"
-import { groqDefaultModelId } from "./groq.js"
-import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
 import { litellmDefaultModelId } from "./lite-llm.js"
 import { mistralDefaultModelId } from "./mistral.js"
 import { moonshotDefaultModelId } from "./moonshot.js"
@@ -54,13 +39,11 @@ import { qwenCodeDefaultModelId } from "./qwen-code.js"
 import { requestyDefaultModelId } from "./requesty.js"
 import { rooDefaultModelId } from "./roo.js"
 import { sambaNovaDefaultModelId } from "./sambanova.js"
-import { unboundDefaultModelId } from "./unbound.js"
 import { vertexDefaultModelId } from "./vertex.js"
 import { vscodeLlmDefaultModelId } from "./vscode-llm.js"
 import { xaiDefaultModelId } from "./xai.js"
 import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js"
 import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js"
-import { deepInfraDefaultModelId } from "./deepinfra.js"
 import { minimaxDefaultModelId } from "./minimax.js"
 
 // Import the ProviderName type from provider-settings to avoid duplication
@@ -80,18 +63,10 @@ export function getProviderDefaultModelId(
 			return openRouterDefaultModelId
 		case "requesty":
 			return requestyDefaultModelId
-		case "unbound":
-			return unboundDefaultModelId
 		case "litellm":
 			return litellmDefaultModelId
 		case "xai":
 			return xaiDefaultModelId
-		case "groq":
-			return groqDefaultModelId
-		case "huggingface":
-			return "meta-llama/Llama-3.3-70B-Instruct"
-		case "chutes":
-			return chutesDefaultModelId
 		case "baseten":
 			return basetenDefaultModelId
 		case "bedrock":
@@ -102,8 +77,6 @@ export function getProviderDefaultModelId(
 			return geminiDefaultModelId
 		case "deepseek":
 			return deepSeekDefaultModelId
-		case "doubao":
-			return doubaoDefaultModelId
 		case "moonshot":
 			return moonshotDefaultModelId
 		case "minimax":
@@ -122,20 +95,12 @@ export function getProviderDefaultModelId(
 			return "" // Ollama uses dynamic model selection
 		case "lmstudio":
 			return "" // LMStudio uses dynamic model selection
-		case "deepinfra":
-			return deepInfraDefaultModelId
 		case "vscode-lm":
 			return vscodeLlmDefaultModelId
-		case "cerebras":
-			return cerebrasDefaultModelId
 		case "sambanova":
 			return sambaNovaDefaultModelId
 		case "fireworks":
 			return fireworksDefaultModelId
-		case "featherless":
-			return featherlessDefaultModelId
-		case "io-intelligence":
-			return ioIntelligenceDefaultModelId
 		case "roo":
 			return rooDefaultModelId
 		case "qwen-code":

+ 0 - 44
packages/types/src/providers/io-intelligence.ts

@@ -1,44 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-export type IOIntelligenceModelId =
-	| "deepseek-ai/DeepSeek-R1-0528"
-	| "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
-	| "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar"
-	| "openai/gpt-oss-120b"
-
-export const ioIntelligenceDefaultModelId: IOIntelligenceModelId = "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
-
-export const ioIntelligenceDefaultBaseUrl = "https://api.intelligence.io.solutions/api/v1"
-
-export const IO_INTELLIGENCE_CACHE_DURATION = 1000 * 60 * 60 // 1 hour
-
-export const ioIntelligenceModels = {
-	"deepseek-ai/DeepSeek-R1-0528": {
-		maxTokens: 8192,
-		contextWindow: 128000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		description: "DeepSeek R1 reasoning model",
-	},
-	"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": {
-		maxTokens: 8192,
-		contextWindow: 430000,
-		supportsImages: true,
-		supportsPromptCache: false,
-		description: "Llama 4 Maverick 17B model",
-	},
-	"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": {
-		maxTokens: 8192,
-		contextWindow: 106000,
-		supportsImages: false,
-		supportsPromptCache: false,
-		description: "Qwen3 Coder 480B specialized for coding",
-	},
-	"openai/gpt-oss-120b": {
-		maxTokens: 8192,
-		contextWindow: 131072,
-		supportsImages: false,
-		supportsPromptCache: false,
-		description: "OpenAI GPT-OSS 120B model",
-	},
-} as const satisfies Record<string, ModelInfo>

+ 0 - 14
packages/types/src/providers/unbound.ts

@@ -1,14 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-export const unboundDefaultModelId = "anthropic/claude-sonnet-4-5"
-
-export const unboundDefaultModelInfo: ModelInfo = {
-	maxTokens: 8192,
-	contextWindow: 200_000,
-	supportsImages: true,
-	supportsPromptCache: true,
-	inputPrice: 3.0,
-	outputPrice: 15.0,
-	cacheWritesPrice: 3.75,
-	cacheReadsPrice: 0.3,
-}

+ 0 - 19
packages/types/src/vscode-extension-host.ts

@@ -47,7 +47,6 @@ export interface ExtensionMessage {
 		| "ollamaModels"
 		| "lmStudioModels"
 		| "vsCodeLmModels"
-		| "huggingFaceModels"
 		| "vsCodeLmApiAvailable"
 		| "updatePrompt"
 		| "systemPrompt"
@@ -144,23 +143,6 @@ export interface ExtensionMessage {
 	ollamaModels?: ModelRecord
 	lmStudioModels?: ModelRecord
 	vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
-	huggingFaceModels?: Array<{
-		id: string
-		object: string
-		created: number
-		owned_by: string
-		providers: Array<{
-			provider: string
-			status: "live" | "staging" | "error"
-			supports_tools?: boolean
-			supports_structured_output?: boolean
-			context_length?: number
-			pricing?: {
-				input: number
-				output: number
-			}
-		}>
-	}>
 	mcpServers?: McpServer[]
 	commits?: GitCommit[]
 	listApiConfig?: ProviderSettingsEntry[]
@@ -469,7 +451,6 @@ export interface WebviewMessage {
 		| "requestRooModels"
 		| "requestRooCreditBalance"
 		| "requestVsCodeLmModels"
-		| "requestHuggingFaceModels"
 		| "openImage"
 		| "saveImage"
 		| "openFile"

+ 2 - 33
pnpm-lock.yaml

@@ -244,8 +244,8 @@ importers:
         specifier: workspace:^
         version: link:../../packages/evals
       '@roo-code/types':
-        specifier: ^1.108.0
-        version: 1.108.0
+        specifier: workspace:^
+        version: link:../../packages/types
       '@tanstack/react-query':
         specifier: ^5.69.0
         version: 5.76.1([email protected])
@@ -752,9 +752,6 @@ importers:
       '@ai-sdk/baseten':
         specifier: ^1.0.31
         version: 1.0.31([email protected])
-      '@ai-sdk/cerebras':
-        specifier: ^2.0.31
-        version: 2.0.31([email protected])
       '@ai-sdk/deepseek':
         specifier: ^2.0.18
         version: 2.0.18([email protected])
@@ -767,9 +764,6 @@ importers:
       '@ai-sdk/google-vertex':
         specifier: ^4.0.45
         version: 4.0.45([email protected])
-      '@ai-sdk/groq':
-        specifier: ^3.0.22
-        version: 3.0.22([email protected])
       '@ai-sdk/mistral':
         specifier: ^3.0.19
         version: 3.0.19([email protected])
@@ -1441,12 +1435,6 @@ packages:
     peerDependencies:
       zod: 3.25.76
 
-  '@ai-sdk/[email protected]':
-    resolution: {integrity: sha512-s7o4BRsbG2RFina4VwHs46RWlQPGCL1CrfOoMomYneJeA0CgpxPigPqwlrupaWWB42KIDDHN5gNOIsLst0oOPg==}
-    engines: {node: '>=18'}
-    peerDependencies:
-      zod: 3.25.76
-
   '@ai-sdk/[email protected]':
     resolution: {integrity: sha512-AwtmFm7acnCsz3z82Yu5QKklSZz+cBwtxrc2hbw47tPF/38xr1zX3Vf/pP627EHwWkLV18UWivIxg0SHPP2w3A==}
     engines: {node: '>=18'}
@@ -1477,12 +1465,6 @@ packages:
     peerDependencies:
       zod: 3.25.76
 
-  '@ai-sdk/[email protected]':
-    resolution: {integrity: sha512-QBkqBmlts2qz2vX54gXeP9IdztMFxZw7xPNwjOjHYhEL7RynzB2aFafPIbAYTVNosrU0YEETxhw9LISjS2TtXw==}
-    engines: {node: '>=18'}
-    peerDependencies:
-      zod: 3.25.76
-
   '@ai-sdk/[email protected]':
     resolution: {integrity: sha512-yd0OJ3fm2YKdwxh1pd9m720sENVVcylAD+Bki8C80QqVpUxGNL1/C4N4JJGb56eCCWr6VU/3gHFe9PKui9n/Hg==}
     engines: {node: '>=18'}
@@ -11067,13 +11049,6 @@ snapshots:
       '@basetenlabs/performance-client': 0.0.10
       zod: 3.25.76
 
-  '@ai-sdk/[email protected]([email protected])':
-    dependencies:
-      '@ai-sdk/openai-compatible': 2.0.28([email protected])
-      '@ai-sdk/provider': 3.0.8
-      '@ai-sdk/provider-utils': 4.0.14([email protected])
-      zod: 3.25.76
-
   '@ai-sdk/[email protected]([email protected])':
     dependencies:
       '@ai-sdk/provider': 3.0.8
@@ -11111,12 +11086,6 @@ snapshots:
       '@ai-sdk/provider-utils': 4.0.14([email protected])
       zod: 3.25.76
 
-  '@ai-sdk/[email protected]([email protected])':
-    dependencies:
-      '@ai-sdk/provider': 3.0.8
-      '@ai-sdk/provider-utils': 4.0.14([email protected])
-      zod: 3.25.76
-
   '@ai-sdk/[email protected]([email protected])':
     dependencies:
       '@ai-sdk/provider': 3.0.8

+ 7 - 31
src/api/index.ts

@@ -1,14 +1,13 @@
 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"
 
 import {
 	AnthropicHandler,
 	AwsBedrockHandler,
-	CerebrasHandler,
 	OpenRouterHandler,
 	VertexHandler,
 	AnthropicVertexHandler,
@@ -21,24 +20,16 @@ import {
 	MoonshotHandler,
 	MistralHandler,
 	VsCodeLmHandler,
-	UnboundHandler,
 	RequestyHandler,
 	FakeAIHandler,
 	XAIHandler,
-	GroqHandler,
-	HuggingFaceHandler,
-	ChutesHandler,
 	LiteLLMHandler,
 	QwenCodeHandler,
 	SambaNovaHandler,
-	IOIntelligenceHandler,
-	DoubaoHandler,
 	ZAiHandler,
 	FireworksHandler,
 	RooHandler,
-	FeatherlessHandler,
 	VercelAiGatewayHandler,
-	DeepInfraHandler,
 	MiniMaxHandler,
 	BasetenHandler,
 } from "./providers"
@@ -51,16 +42,13 @@ export interface SingleCompletionHandler {
 export interface ApiHandlerCreateMessageMetadata {
 	/**
 	 * Task ID used for tracking and provider-specific features:
-	 * - DeepInfra: Used as prompt_cache_key for caching
 	 * - Roo: Sent as X-Roo-Task-ID header
 	 * - Requesty: Sent as trace_id
-	 * - Unbound: Sent in unbound_metadata
 	 */
 	taskId: string
 	/**
 	 * Current mode slug for provider-specific tracking:
 	 * - Requesty: Sent in extra metadata
-	 * - Unbound: Sent in unbound_metadata
 	 */
 	mode?: string
 	suppressPreviousResponseId?: boolean
@@ -131,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)
@@ -156,8 +150,6 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new OpenAiNativeHandler(options)
 		case "deepseek":
 			return new DeepSeekHandler(options)
-		case "doubao":
-			return new DoubaoHandler(options)
 		case "qwen-code":
 			return new QwenCodeHandler(options)
 		case "moonshot":
@@ -166,40 +158,24 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new VsCodeLmHandler(options)
 		case "mistral":
 			return new MistralHandler(options)
-		case "unbound":
-			return new UnboundHandler(options)
 		case "requesty":
 			return new RequestyHandler(options)
 		case "fake-ai":
 			return new FakeAIHandler(options)
 		case "xai":
 			return new XAIHandler(options)
-		case "groq":
-			return new GroqHandler(options)
-		case "deepinfra":
-			return new DeepInfraHandler(options)
-		case "huggingface":
-			return new HuggingFaceHandler(options)
-		case "chutes":
-			return new ChutesHandler(options)
 		case "litellm":
 			return new LiteLLMHandler(options)
-		case "cerebras":
-			return new CerebrasHandler(options)
 		case "sambanova":
 			return new SambaNovaHandler(options)
 		case "zai":
 			return new ZAiHandler(options)
 		case "fireworks":
 			return new FireworksHandler(options)
-		case "io-intelligence":
-			return new IOIntelligenceHandler(options)
 		case "roo":
 			// Never throw exceptions from provider constructors
 			// The provider-proxy server will handle authentication and return appropriate error codes
 			return new RooHandler(options)
-		case "featherless":
-			return new FeatherlessHandler(options)
 		case "vercel-ai-gateway":
 			return new VercelAiGatewayHandler(options)
 		case "minimax":

+ 0 - 455
src/api/providers/__tests__/cerebras.spec.ts

@@ -1,455 +0,0 @@
-// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
-const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
-	mockStreamText: vi.fn(),
-	mockGenerateText: vi.fn(),
-}))
-
-vi.mock("ai", async (importOriginal) => {
-	const actual = await importOriginal<typeof import("ai")>()
-	return {
-		...actual,
-		streamText: mockStreamText,
-		generateText: mockGenerateText,
-	}
-})
-
-vi.mock("@ai-sdk/cerebras", () => ({
-	createCerebras: vi.fn(() => {
-		// Return a function that returns a mock language model
-		return vi.fn(() => ({
-			modelId: "llama-3.3-70b",
-			provider: "cerebras",
-		}))
-	}),
-}))
-
-import type { Anthropic } from "@anthropic-ai/sdk"
-
-import { cerebrasDefaultModelId, cerebrasModels, type CerebrasModelId } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../../shared/api"
-
-import { CerebrasHandler } from "../cerebras"
-
-describe("CerebrasHandler", () => {
-	let handler: CerebrasHandler
-	let mockOptions: ApiHandlerOptions
-
-	beforeEach(() => {
-		mockOptions = {
-			cerebrasApiKey: "test-api-key",
-			apiModelId: "llama-3.3-70b" as CerebrasModelId,
-		}
-		handler = new CerebrasHandler(mockOptions)
-		vi.clearAllMocks()
-	})
-
-	describe("constructor", () => {
-		it("should initialize with provided options", () => {
-			expect(handler).toBeInstanceOf(CerebrasHandler)
-			expect(handler.getModel().id).toBe(mockOptions.apiModelId)
-		})
-
-		it("should use default model ID if not provided", () => {
-			const handlerWithoutModel = new CerebrasHandler({
-				...mockOptions,
-				apiModelId: undefined,
-			})
-			expect(handlerWithoutModel.getModel().id).toBe(cerebrasDefaultModelId)
-		})
-	})
-
-	describe("getModel", () => {
-		it("should return model info for valid model ID", () => {
-			const model = handler.getModel()
-			expect(model.id).toBe(mockOptions.apiModelId)
-			expect(model.info).toBeDefined()
-			expect(model.info.maxTokens).toBe(16384)
-			expect(model.info.contextWindow).toBe(64000)
-			expect(model.info.supportsImages).toBe(false)
-			expect(model.info.supportsPromptCache).toBe(false)
-		})
-
-		it("should return provided model ID with default model info if model does not exist", () => {
-			const handlerWithInvalidModel = new CerebrasHandler({
-				...mockOptions,
-				apiModelId: "invalid-model",
-			})
-			const model = handlerWithInvalidModel.getModel()
-			expect(model.id).toBe("invalid-model") // Returns provided ID
-			expect(model.info).toBeDefined()
-			// Should have the same base properties as default model
-			expect(model.info.contextWindow).toBe(cerebrasModels[cerebrasDefaultModelId].contextWindow)
-		})
-
-		it("should return default model if no model ID is provided", () => {
-			const handlerWithoutModel = new CerebrasHandler({
-				...mockOptions,
-				apiModelId: undefined,
-			})
-			const model = handlerWithoutModel.getModel()
-			expect(model.id).toBe(cerebrasDefaultModelId)
-			expect(model.info).toBeDefined()
-		})
-
-		it("should include model parameters from getModelParams", () => {
-			const model = handler.getModel()
-			expect(model).toHaveProperty("temperature")
-			expect(model).toHaveProperty("maxTokens")
-		})
-	})
-
-	describe("createMessage", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [
-					{
-						type: "text" as const,
-						text: "Hello!",
-					},
-				],
-			},
-		]
-
-		it("should handle streaming responses", async () => {
-			// Mock the fullStream async generator
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			// Mock usage promise
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks.length).toBeGreaterThan(0)
-			const textChunks = chunks.filter((chunk) => chunk.type === "text")
-			expect(textChunks).toHaveLength(1)
-			expect(textChunks[0].text).toBe("Test response")
-		})
-
-		it("should include usage information", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].inputTokens).toBe(10)
-			expect(usageChunks[0].outputTokens).toBe(5)
-		})
-
-		it("should handle reasoning content in streaming responses", async () => {
-			// Mock the fullStream async generator with reasoning content
-			async function* mockFullStream() {
-				yield { type: "reasoning", text: "Let me think about this..." }
-				yield { type: "reasoning", text: " I'll analyze step by step." }
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-				details: {
-					reasoningTokens: 15,
-				},
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			// Should have reasoning chunks
-			const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
-			expect(reasoningChunks.length).toBe(2)
-			expect(reasoningChunks[0].text).toBe("Let me think about this...")
-			expect(reasoningChunks[1].text).toBe(" I'll analyze step by step.")
-
-			// Should also have text chunks
-			const textChunks = chunks.filter((chunk) => chunk.type === "text")
-			expect(textChunks.length).toBe(1)
-			expect(textChunks[0].text).toBe("Test response")
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should complete a prompt using generateText", async () => {
-			mockGenerateText.mockResolvedValue({
-				text: "Test completion",
-			})
-
-			const result = await handler.completePrompt("Test prompt")
-
-			expect(result).toBe("Test completion")
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					prompt: "Test prompt",
-				}),
-			)
-		})
-	})
-
-	describe("processUsageMetrics", () => {
-		it("should correctly process usage metrics", () => {
-			// We need to access the protected method, so we'll create a test subclass
-			class TestCerebrasHandler extends CerebrasHandler {
-				public testProcessUsageMetrics(usage: any) {
-					return this.processUsageMetrics(usage)
-				}
-			}
-
-			const testHandler = new TestCerebrasHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-				details: {
-					cachedInputTokens: 20,
-					reasoningTokens: 30,
-				},
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage)
-
-			expect(result.type).toBe("usage")
-			expect(result.inputTokens).toBe(100)
-			expect(result.outputTokens).toBe(50)
-			expect(result.cacheReadTokens).toBe(20)
-			expect(result.reasoningTokens).toBe(30)
-		})
-
-		it("should handle missing cache metrics gracefully", () => {
-			class TestCerebrasHandler extends CerebrasHandler {
-				public testProcessUsageMetrics(usage: any) {
-					return this.processUsageMetrics(usage)
-				}
-			}
-
-			const testHandler = new TestCerebrasHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage)
-
-			expect(result.type).toBe("usage")
-			expect(result.inputTokens).toBe(100)
-			expect(result.outputTokens).toBe(50)
-			expect(result.cacheReadTokens).toBeUndefined()
-			expect(result.reasoningTokens).toBeUndefined()
-		})
-	})
-
-	describe("getMaxOutputTokens", () => {
-		it("should return maxTokens from model info", () => {
-			class TestCerebrasHandler extends CerebrasHandler {
-				public testGetMaxOutputTokens() {
-					return this.getMaxOutputTokens()
-				}
-			}
-
-			const testHandler = new TestCerebrasHandler(mockOptions)
-			const result = testHandler.testGetMaxOutputTokens()
-
-			// llama-3.3-70b maxTokens is 16384
-			expect(result).toBe(16384)
-		})
-
-		it("should use modelMaxTokens when provided", () => {
-			class TestCerebrasHandler extends CerebrasHandler {
-				public testGetMaxOutputTokens() {
-					return this.getMaxOutputTokens()
-				}
-			}
-
-			const customMaxTokens = 5000
-			const testHandler = new TestCerebrasHandler({
-				...mockOptions,
-				modelMaxTokens: customMaxTokens,
-			})
-
-			const result = testHandler.testGetMaxOutputTokens()
-			expect(result).toBe(customMaxTokens)
-		})
-
-		it("should fall back to modelInfo.maxTokens when modelMaxTokens is not provided", () => {
-			class TestCerebrasHandler extends CerebrasHandler {
-				public testGetMaxOutputTokens() {
-					return this.getMaxOutputTokens()
-				}
-			}
-
-			const testHandler = new TestCerebrasHandler(mockOptions)
-			const result = testHandler.testGetMaxOutputTokens()
-
-			// llama-3.3-70b has maxTokens of 16384
-			expect(result).toBe(16384)
-		})
-	})
-
-	describe("tool handling", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [{ type: "text" as const, text: "Hello!" }],
-			},
-		]
-
-		it("should handle tool calls in streaming", async () => {
-			async function* mockFullStream() {
-				yield {
-					type: "tool-input-start",
-					id: "tool-call-1",
-					toolName: "read_file",
-				}
-				yield {
-					type: "tool-input-delta",
-					id: "tool-call-1",
-					delta: '{"path":"test.ts"}',
-				}
-				yield {
-					type: "tool-input-end",
-					id: "tool-call-1",
-				}
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages, {
-				taskId: "test-task",
-				tools: [
-					{
-						type: "function",
-						function: {
-							name: "read_file",
-							description: "Read a file",
-							parameters: {
-								type: "object",
-								properties: { path: { type: "string" } },
-								required: ["path"],
-							},
-						},
-					},
-				],
-			})
-
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const toolCallStartChunks = chunks.filter((c) => c.type === "tool_call_start")
-			const toolCallDeltaChunks = chunks.filter((c) => c.type === "tool_call_delta")
-			const toolCallEndChunks = chunks.filter((c) => c.type === "tool_call_end")
-
-			expect(toolCallStartChunks.length).toBe(1)
-			expect(toolCallStartChunks[0].id).toBe("tool-call-1")
-			expect(toolCallStartChunks[0].name).toBe("read_file")
-
-			expect(toolCallDeltaChunks.length).toBe(1)
-			expect(toolCallDeltaChunks[0].delta).toBe('{"path":"test.ts"}')
-
-			expect(toolCallEndChunks.length).toBe(1)
-			expect(toolCallEndChunks[0].id).toBe("tool-call-1")
-		})
-
-		it("should ignore tool-call events to prevent duplicate tools in UI", async () => {
-			// tool-call events are intentionally ignored because tool-input-start/delta/end
-			// already provide complete tool call information. Emitting tool-call would cause
-			// duplicate tools in the UI for AI SDK providers (e.g., DeepSeek, Moonshot, Cerebras).
-			async function* mockFullStream() {
-				yield {
-					type: "tool-call",
-					toolCallId: "tool-call-1",
-					toolName: "read_file",
-					input: { path: "test.ts" },
-				}
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages, {
-				taskId: "test-task",
-				tools: [
-					{
-						type: "function",
-						function: {
-							name: "read_file",
-							description: "Read a file",
-							parameters: {
-								type: "object",
-								properties: { path: { type: "string" } },
-								required: ["path"],
-							},
-						},
-					},
-				],
-			})
-
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			// tool-call events are ignored, so no tool_call chunks should be emitted
-			const toolCallChunks = chunks.filter((c) => c.type === "tool_call")
-			expect(toolCallChunks.length).toBe(0)
-		})
-	})
-})

+ 0 - 490
src/api/providers/__tests__/chutes.spec.ts

@@ -1,490 +0,0 @@
-// npx vitest run api/providers/__tests__/chutes.spec.ts
-
-const { mockStreamText, mockGenerateText, mockGetModels, mockGetModelsFromCache } = vi.hoisted(() => ({
-	mockStreamText: vi.fn(),
-	mockGenerateText: vi.fn(),
-	mockGetModels: vi.fn(),
-	mockGetModelsFromCache: vi.fn(),
-}))
-
-vi.mock("ai", async (importOriginal) => {
-	const actual = await importOriginal<typeof import("ai")>()
-	return {
-		...actual,
-		streamText: mockStreamText,
-		generateText: mockGenerateText,
-	}
-})
-
-vi.mock("@ai-sdk/openai-compatible", () => ({
-	createOpenAICompatible: vi.fn(() => {
-		return vi.fn((modelId: string) => ({
-			modelId,
-			provider: "chutes",
-		}))
-	}),
-}))
-
-vi.mock("../fetchers/modelCache", () => ({
-	getModels: mockGetModels,
-	getModelsFromCache: mockGetModelsFromCache,
-}))
-
-import type { Anthropic } from "@anthropic-ai/sdk"
-
-import { chutesDefaultModelId, chutesDefaultModelInfo, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types"
-
-import { ChutesHandler } from "../chutes"
-
-describe("ChutesHandler", () => {
-	let handler: ChutesHandler
-
-	beforeEach(() => {
-		vi.clearAllMocks()
-		mockGetModels.mockResolvedValue({
-			[chutesDefaultModelId]: chutesDefaultModelInfo,
-		})
-		mockGetModelsFromCache.mockReturnValue(undefined)
-		handler = new ChutesHandler({ chutesApiKey: "test-key" })
-	})
-
-	afterEach(() => {
-		vi.restoreAllMocks()
-	})
-
-	describe("constructor", () => {
-		it("should initialize with provided options", () => {
-			expect(handler).toBeInstanceOf(ChutesHandler)
-		})
-
-		it("should use default model when no model ID is provided", () => {
-			const model = handler.getModel()
-			expect(model.id).toBe(chutesDefaultModelId)
-		})
-	})
-
-	describe("getModel", () => {
-		it("should return default model when no model is specified and no cache", () => {
-			const model = handler.getModel()
-			expect(model.id).toBe(chutesDefaultModelId)
-			expect(model.info).toEqual(
-				expect.objectContaining({
-					...chutesDefaultModelInfo,
-				}),
-			)
-		})
-
-		it("should return model info from fetched models", async () => {
-			const testModelInfo = {
-				maxTokens: 4096,
-				contextWindow: 128000,
-				supportsImages: false,
-				supportsPromptCache: false,
-			}
-			mockGetModels.mockResolvedValue({
-				"some-model": testModelInfo,
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "some-model",
-				chutesApiKey: "test-key",
-			})
-			const model = await handlerWithModel.fetchModel()
-			expect(model.id).toBe("some-model")
-			expect(model.info).toEqual(expect.objectContaining(testModelInfo))
-		})
-
-		it("should fall back to global cache when instance models are empty", () => {
-			const cachedInfo = {
-				maxTokens: 2048,
-				contextWindow: 64000,
-				supportsImages: false,
-				supportsPromptCache: false,
-			}
-			mockGetModelsFromCache.mockReturnValue({
-				"cached-model": cachedInfo,
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "cached-model",
-				chutesApiKey: "test-key",
-			})
-			const model = handlerWithModel.getModel()
-			expect(model.id).toBe("cached-model")
-			expect(model.info).toEqual(expect.objectContaining(cachedInfo))
-		})
-
-		it("should apply DeepSeek default temperature for R1 models", () => {
-			const r1Info = {
-				maxTokens: 32768,
-				contextWindow: 163840,
-				supportsImages: false,
-				supportsPromptCache: false,
-			}
-			mockGetModelsFromCache.mockReturnValue({
-				"deepseek-ai/DeepSeek-R1-0528": r1Info,
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "deepseek-ai/DeepSeek-R1-0528",
-				chutesApiKey: "test-key",
-			})
-			const model = handlerWithModel.getModel()
-			expect(model.info.defaultTemperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE)
-			expect(model.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE)
-		})
-
-		it("should use default temperature for non-DeepSeek models", () => {
-			const modelInfo = {
-				maxTokens: 4096,
-				contextWindow: 128000,
-				supportsImages: false,
-				supportsPromptCache: false,
-			}
-			mockGetModelsFromCache.mockReturnValue({
-				"unsloth/Llama-3.3-70B-Instruct": modelInfo,
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "unsloth/Llama-3.3-70B-Instruct",
-				chutesApiKey: "test-key",
-			})
-			const model = handlerWithModel.getModel()
-			expect(model.info.defaultTemperature).toBe(0.5)
-			expect(model.temperature).toBe(0.5)
-		})
-	})
-
-	describe("fetchModel", () => {
-		it("should fetch models and return the resolved model", async () => {
-			const model = await handler.fetchModel()
-			expect(mockGetModels).toHaveBeenCalledWith(
-				expect.objectContaining({
-					provider: "chutes",
-				}),
-			)
-			expect(model.id).toBe(chutesDefaultModelId)
-		})
-	})
-
-	describe("createMessage", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]
-
-		it("should handle non-DeepSeek models with standard streaming", async () => {
-			mockGetModels.mockResolvedValue({
-				"some-other-model": { maxTokens: 1024, contextWindow: 8192, supportsPromptCache: false },
-			})
-
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "some-other-model",
-				chutesApiKey: "test-key",
-			})
-
-			const stream = handlerWithModel.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks).toEqual([
-				{ type: "text", text: "Test response" },
-				{
-					type: "usage",
-					inputTokens: 10,
-					outputTokens: 5,
-					cacheReadTokens: undefined,
-					reasoningTokens: undefined,
-				},
-			])
-		})
-
-		it("should handle DeepSeek R1 reasoning format with TagMatcher", async () => {
-			mockGetModels.mockResolvedValue({
-				"deepseek-ai/DeepSeek-R1-0528": {
-					maxTokens: 32768,
-					contextWindow: 163840,
-					supportsImages: false,
-					supportsPromptCache: false,
-				},
-			})
-
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "<think>Thinking..." }
-				yield { type: "text-delta", text: "</think>Hello" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "deepseek-ai/DeepSeek-R1-0528",
-				chutesApiKey: "test-key",
-			})
-
-			const stream = handlerWithModel.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks).toEqual([
-				{ type: "reasoning", text: "Thinking..." },
-				{ type: "text", text: "Hello" },
-				{
-					type: "usage",
-					inputTokens: 10,
-					outputTokens: 5,
-					cacheReadTokens: undefined,
-					reasoningTokens: undefined,
-				},
-			])
-		})
-
-		it("should handle tool calls in R1 path", async () => {
-			mockGetModels.mockResolvedValue({
-				"deepseek-ai/DeepSeek-R1-0528": {
-					maxTokens: 32768,
-					contextWindow: 163840,
-					supportsImages: false,
-					supportsPromptCache: false,
-				},
-			})
-
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Let me help" }
-				yield {
-					type: "tool-input-start",
-					id: "call_123",
-					toolName: "test_tool",
-				}
-				yield {
-					type: "tool-input-delta",
-					id: "call_123",
-					delta: '{"arg":"value"}',
-				}
-				yield {
-					type: "tool-input-end",
-					id: "call_123",
-				}
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 15,
-				outputTokens: 10,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "deepseek-ai/DeepSeek-R1-0528",
-				chutesApiKey: "test-key",
-			})
-
-			const stream = handlerWithModel.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks).toContainEqual({ type: "text", text: "Let me help" })
-			expect(chunks).toContainEqual({
-				type: "tool_call_start",
-				id: "call_123",
-				name: "test_tool",
-			})
-			expect(chunks).toContainEqual({
-				type: "tool_call_delta",
-				id: "call_123",
-				delta: '{"arg":"value"}',
-			})
-			expect(chunks).toContainEqual({
-				type: "tool_call_end",
-				id: "call_123",
-			})
-		})
-
-		it("should merge system prompt into first user message for R1 path", async () => {
-			mockGetModels.mockResolvedValue({
-				"deepseek-ai/DeepSeek-R1-0528": {
-					maxTokens: 32768,
-					contextWindow: 163840,
-					supportsImages: false,
-					supportsPromptCache: false,
-				},
-			})
-
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Response" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 5, outputTokens: 3 }),
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "deepseek-ai/DeepSeek-R1-0528",
-				chutesApiKey: "test-key",
-			})
-
-			const stream = handlerWithModel.createMessage(systemPrompt, messages)
-			for await (const _ of stream) {
-				// consume
-			}
-
-			expect(mockStreamText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					messages: expect.any(Array),
-				}),
-			)
-
-			const callArgs = mockStreamText.mock.calls[0][0]
-			expect(callArgs.system).toBeUndefined()
-		})
-
-		it("should pass system prompt separately for non-R1 path", async () => {
-			mockGetModels.mockResolvedValue({
-				"some-model": { maxTokens: 1024, contextWindow: 8192, supportsPromptCache: false },
-			})
-
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Response" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 5, outputTokens: 3 }),
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "some-model",
-				chutesApiKey: "test-key",
-			})
-
-			const stream = handlerWithModel.createMessage(systemPrompt, messages)
-			for await (const _ of stream) {
-				// consume
-			}
-
-			expect(mockStreamText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					system: systemPrompt,
-				}),
-			)
-		})
-
-		it("should include usage information from stream", async () => {
-			mockGetModels.mockResolvedValue({
-				"some-model": { maxTokens: 1024, contextWindow: 8192, supportsPromptCache: false },
-			})
-
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Hello" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({
-					inputTokens: 20,
-					outputTokens: 10,
-				}),
-			})
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "some-model",
-				chutesApiKey: "test-key",
-			})
-
-			const stream = handlerWithModel.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((c) => c.type === "usage")
-			expect(usageChunks).toHaveLength(1)
-			expect(usageChunks[0].inputTokens).toBe(20)
-			expect(usageChunks[0].outputTokens).toBe(10)
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should return text from generateText", async () => {
-			const expectedResponse = "This is a test response from Chutes"
-			mockGenerateText.mockResolvedValue({ text: expectedResponse })
-
-			const result = await handler.completePrompt("test prompt")
-			expect(result).toBe(expectedResponse)
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					prompt: "test prompt",
-				}),
-			)
-		})
-
-		it("should handle errors in completePrompt", async () => {
-			const errorMessage = "Chutes API error"
-			mockGenerateText.mockRejectedValue(new Error(errorMessage))
-			await expect(handler.completePrompt("test prompt")).rejects.toThrow(
-				`Chutes completion error: ${errorMessage}`,
-			)
-		})
-
-		it("should pass temperature for R1 models in completePrompt", async () => {
-			mockGetModels.mockResolvedValue({
-				"deepseek-ai/DeepSeek-R1-0528": {
-					maxTokens: 32768,
-					contextWindow: 163840,
-					supportsImages: false,
-					supportsPromptCache: false,
-				},
-			})
-
-			mockGenerateText.mockResolvedValue({ text: "response" })
-
-			const handlerWithModel = new ChutesHandler({
-				apiModelId: "deepseek-ai/DeepSeek-R1-0528",
-				chutesApiKey: "test-key",
-			})
-
-			await handlerWithModel.completePrompt("test prompt")
-
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					temperature: DEEP_SEEK_DEFAULT_TEMPERATURE,
-				}),
-			)
-		})
-	})
-
-	describe("isAiSdkProvider", () => {
-		it("should return true", () => {
-			expect(handler.isAiSdkProvider()).toBe(true)
-		})
-	})
-})

+ 0 - 386
src/api/providers/__tests__/deepinfra.spec.ts

@@ -1,386 +0,0 @@
-// npx vitest api/providers/__tests__/deepinfra.spec.ts
-
-import { deepInfraDefaultModelId, deepInfraDefaultModelInfo } from "@roo-code/types"
-
-const mockCreate = vitest.fn()
-const mockWithResponse = vitest.fn()
-
-vitest.mock("openai", () => {
-	const mockConstructor = vitest.fn()
-
-	return {
-		__esModule: true,
-		default: mockConstructor.mockImplementation(() => ({
-			chat: {
-				completions: {
-					create: mockCreate.mockImplementation(() => ({
-						withResponse: mockWithResponse,
-					})),
-				},
-			},
-		})),
-	}
-})
-
-vitest.mock("../fetchers/modelCache", () => ({
-	getModels: vitest.fn().mockResolvedValue({
-		[deepInfraDefaultModelId]: deepInfraDefaultModelInfo,
-	}),
-	getModelsFromCache: vitest.fn().mockReturnValue(undefined),
-}))
-
-import OpenAI from "openai"
-import { DeepInfraHandler } from "../deepinfra"
-
-describe("DeepInfraHandler", () => {
-	let handler: DeepInfraHandler
-
-	beforeEach(() => {
-		vi.clearAllMocks()
-		mockCreate.mockClear()
-		mockWithResponse.mockClear()
-
-		handler = new DeepInfraHandler({})
-	})
-
-	it("should use the correct DeepInfra base URL", () => {
-		expect(OpenAI).toHaveBeenCalledWith(
-			expect.objectContaining({
-				baseURL: "https://api.deepinfra.com/v1/openai",
-			}),
-		)
-	})
-
-	it("should use the provided API key", () => {
-		vi.clearAllMocks()
-
-		const deepInfraApiKey = "test-api-key"
-		new DeepInfraHandler({ deepInfraApiKey })
-
-		expect(OpenAI).toHaveBeenCalledWith(
-			expect.objectContaining({
-				apiKey: deepInfraApiKey,
-			}),
-		)
-	})
-
-	it("should return default model when no model is specified", () => {
-		const model = handler.getModel()
-		expect(model.id).toBe(deepInfraDefaultModelId)
-		expect(model.info).toEqual(deepInfraDefaultModelInfo)
-	})
-
-	it("createMessage should yield text content from stream", async () => {
-		const testContent = "This is test content"
-
-		mockWithResponse.mockResolvedValueOnce({
-			data: {
-				[Symbol.asyncIterator]: () => ({
-					next: vi
-						.fn()
-						.mockResolvedValueOnce({
-							done: false,
-							value: {
-								choices: [{ delta: { content: testContent } }],
-							},
-						})
-						.mockResolvedValueOnce({ done: true }),
-				}),
-			},
-		})
-
-		const stream = handler.createMessage("system prompt", [])
-		const firstChunk = await stream.next()
-
-		expect(firstChunk.done).toBe(false)
-		expect(firstChunk.value).toEqual({
-			type: "text",
-			text: testContent,
-		})
-	})
-
-	it("createMessage should yield reasoning content from stream", async () => {
-		const testReasoning = "Test reasoning content"
-
-		mockWithResponse.mockResolvedValueOnce({
-			data: {
-				[Symbol.asyncIterator]: () => ({
-					next: vi
-						.fn()
-						.mockResolvedValueOnce({
-							done: false,
-							value: {
-								choices: [{ delta: { reasoning_content: testReasoning } }],
-							},
-						})
-						.mockResolvedValueOnce({ done: true }),
-				}),
-			},
-		})
-
-		const stream = handler.createMessage("system prompt", [])
-		const firstChunk = await stream.next()
-
-		expect(firstChunk.done).toBe(false)
-		expect(firstChunk.value).toEqual({
-			type: "reasoning",
-			text: testReasoning,
-		})
-	})
-
-	it("createMessage should yield usage data from stream", async () => {
-		mockWithResponse.mockResolvedValueOnce({
-			data: {
-				[Symbol.asyncIterator]: () => ({
-					next: vi
-						.fn()
-						.mockResolvedValueOnce({
-							done: false,
-							value: {
-								choices: [{ delta: {} }],
-								usage: {
-									prompt_tokens: 10,
-									completion_tokens: 20,
-									prompt_tokens_details: {
-										cache_write_tokens: 15,
-										cached_tokens: 5,
-									},
-								},
-							},
-						})
-						.mockResolvedValueOnce({ done: true }),
-				}),
-			},
-		})
-
-		const stream = handler.createMessage("system prompt", [])
-		const firstChunk = await stream.next()
-
-		expect(firstChunk.done).toBe(false)
-		expect(firstChunk.value).toEqual({
-			type: "usage",
-			inputTokens: 10,
-			outputTokens: 20,
-			cacheWriteTokens: 15,
-			cacheReadTokens: 5,
-			totalCost: expect.any(Number),
-		})
-	})
-
-	describe("Native Tool Calling", () => {
-		const testTools = [
-			{
-				type: "function" as const,
-				function: {
-					name: "test_tool",
-					description: "A test tool",
-					parameters: {
-						type: "object",
-						properties: {
-							arg1: { type: "string", description: "First argument" },
-						},
-						required: ["arg1"],
-					},
-				},
-			},
-		]
-
-		it("should include tools in request when model supports native tools and tools are provided", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-			})
-			await messageGenerator.next()
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					tools: expect.arrayContaining([
-						expect.objectContaining({
-							type: "function",
-							function: expect.objectContaining({
-								name: "test_tool",
-							}),
-						}),
-					]),
-				}),
-			)
-			// parallel_tool_calls should be true by default when not explicitly set
-			const callArgs = mockCreate.mock.calls[0][0]
-			expect(callArgs).toHaveProperty("parallel_tool_calls", true)
-		})
-
-		it("should include tool_choice when provided", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-				tool_choice: "auto",
-			})
-			await messageGenerator.next()
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					tool_choice: "auto",
-				}),
-			)
-		})
-
-		it("should always include tools and tool_choice in request (tools are always present after PR #10841)", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-			})
-			await messageGenerator.next()
-
-			const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
-			// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
-			expect(callArgs).toHaveProperty("tools")
-			expect(callArgs).toHaveProperty("tool_choice")
-			// parallel_tool_calls should be true by default when not explicitly set
-			expect(callArgs).toHaveProperty("parallel_tool_calls", true)
-		})
-
-		it("should yield tool_call_partial chunks during streaming", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						next: vi
-							.fn()
-							.mockResolvedValueOnce({
-								done: false,
-								value: {
-									choices: [
-										{
-											delta: {
-												tool_calls: [
-													{
-														index: 0,
-														id: "call_123",
-														function: {
-															name: "test_tool",
-															arguments: '{"arg1":',
-														},
-													},
-												],
-											},
-										},
-									],
-								},
-							})
-							.mockResolvedValueOnce({
-								done: false,
-								value: {
-									choices: [
-										{
-											delta: {
-												tool_calls: [
-													{
-														index: 0,
-														function: {
-															arguments: '"value"}',
-														},
-													},
-												],
-											},
-										},
-									],
-								},
-							})
-							.mockResolvedValueOnce({ done: true }),
-					}),
-				},
-			})
-
-			const stream = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-			})
-
-			const chunks = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks).toContainEqual({
-				type: "tool_call_partial",
-				index: 0,
-				id: "call_123",
-				name: "test_tool",
-				arguments: '{"arg1":',
-			})
-
-			expect(chunks).toContainEqual({
-				type: "tool_call_partial",
-				index: 0,
-				id: undefined,
-				name: undefined,
-				arguments: '"value"}',
-			})
-		})
-
-		it("should set parallel_tool_calls based on metadata", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-				parallelToolCalls: true,
-			})
-			await messageGenerator.next()
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					parallel_tool_calls: true,
-				}),
-			)
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should return text from API", async () => {
-			const expectedResponse = "This is a test response"
-			mockCreate.mockResolvedValueOnce({
-				choices: [{ message: { content: expectedResponse } }],
-			})
-
-			const result = await handler.completePrompt("test prompt")
-			expect(result).toBe(expectedResponse)
-		})
-	})
-})

+ 0 - 356
src/api/providers/__tests__/featherless.spec.ts

@@ -1,356 +0,0 @@
-// npx vitest run api/providers/__tests__/featherless.spec.ts
-
-const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
-	mockStreamText: vi.fn(),
-	mockGenerateText: vi.fn(),
-}))
-
-vi.mock("ai", async (importOriginal) => {
-	const actual = await importOriginal<typeof import("ai")>()
-	return {
-		...actual,
-		streamText: mockStreamText,
-		generateText: mockGenerateText,
-	}
-})
-
-vi.mock("@ai-sdk/openai-compatible", () => ({
-	createOpenAICompatible: vi.fn(() => {
-		return vi.fn(() => ({
-			modelId: "featherless-model",
-			provider: "Featherless",
-		}))
-	}),
-}))
-
-import type { Anthropic } from "@anthropic-ai/sdk"
-
-import { type FeatherlessModelId, featherlessDefaultModelId, featherlessModels } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../../shared/api"
-
-import { FeatherlessHandler } from "../featherless"
-
-describe("FeatherlessHandler", () => {
-	let handler: FeatherlessHandler
-	let mockOptions: ApiHandlerOptions
-
-	beforeEach(() => {
-		mockOptions = {
-			featherlessApiKey: "test-api-key",
-		}
-		handler = new FeatherlessHandler(mockOptions)
-		vi.clearAllMocks()
-	})
-
-	describe("constructor", () => {
-		it("should initialize with provided options", () => {
-			expect(handler).toBeInstanceOf(FeatherlessHandler)
-			expect(handler.getModel().id).toBe(featherlessDefaultModelId)
-		})
-
-		it("should use specified model ID when provided", () => {
-			const testModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"
-			const handlerWithModel = new FeatherlessHandler({
-				apiModelId: testModelId,
-				featherlessApiKey: "test-api-key",
-			})
-			expect(handlerWithModel.getModel().id).toBe(testModelId)
-		})
-	})
-
-	describe("getModel", () => {
-		it("should return default model when no model is specified", () => {
-			const model = handler.getModel()
-			expect(model.id).toBe(featherlessDefaultModelId)
-			expect(model.info).toEqual(expect.objectContaining(featherlessModels[featherlessDefaultModelId]))
-		})
-
-		it("should return specified model when valid model is provided", () => {
-			const testModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"
-			const handlerWithModel = new FeatherlessHandler({
-				apiModelId: testModelId,
-				featherlessApiKey: "test-api-key",
-			})
-			const model = handlerWithModel.getModel()
-			expect(model.id).toBe(testModelId)
-			expect(model.info).toEqual(expect.objectContaining(featherlessModels[testModelId]))
-		})
-
-		it("should use default temperature for non-DeepSeek models", () => {
-			const testModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"
-			const handlerWithModel = new FeatherlessHandler({
-				apiModelId: testModelId,
-				featherlessApiKey: "test-api-key",
-			})
-			const model = handlerWithModel.getModel()
-			expect(model.temperature).toBe(0.5)
-		})
-
-		it("should include model parameters from getModelParams", () => {
-			const model = handler.getModel()
-			expect(model).toHaveProperty("temperature")
-			expect(model).toHaveProperty("maxTokens")
-		})
-	})
-
-	describe("createMessage", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [
-					{
-						type: "text" as const,
-						text: "Hello!",
-					},
-				],
-			},
-		]
-
-		it("should handle streaming responses for non-R1 models", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks.length).toBeGreaterThan(0)
-			const textChunks = chunks.filter((chunk) => chunk.type === "text")
-			expect(textChunks).toHaveLength(1)
-			expect(textChunks[0].text).toBe("Test response")
-		})
-
-		it("should include usage information", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].inputTokens).toBe(10)
-			expect(usageChunks[0].outputTokens).toBe(5)
-		})
-
-		it("should handle reasoning format from DeepSeek-R1 models using TagMatcher", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "<think>Thinking..." }
-				yield { type: "text-delta", text: "</think>Hello" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			vi.spyOn(handler, "getModel").mockReturnValue({
-				id: "some-DeepSeek-R1-model",
-				info: { maxTokens: 1024, temperature: 0.6 },
-				maxTokens: 1024,
-				temperature: 0.6,
-			} as any)
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks[0]).toEqual({ type: "reasoning", text: "Thinking..." })
-			expect(chunks[1]).toEqual({ type: "text", text: "Hello" })
-			expect(chunks[2]).toMatchObject({ type: "usage", inputTokens: 10, outputTokens: 5 })
-		})
-
-		it("should delegate to super.createMessage for non-DeepSeek-R1 models", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Standard response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 15,
-				outputTokens: 8,
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			vi.spyOn(handler, "getModel").mockReturnValue({
-				id: "some-other-model",
-				info: { maxTokens: 1024, temperature: 0.5 },
-				maxTokens: 1024,
-				temperature: 0.5,
-			} as any)
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks[0]).toEqual({ type: "text", text: "Standard response" })
-			expect(chunks[1]).toMatchObject({ type: "usage", inputTokens: 15, outputTokens: 8 })
-		})
-
-		it("should pass correct model to streamText for R1 path", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "response" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
-			})
-
-			vi.spyOn(handler, "getModel").mockReturnValue({
-				id: "some-DeepSeek-R1-model",
-				info: { maxTokens: 2048, temperature: 0.6 },
-				maxTokens: 2048,
-				temperature: 0.6,
-			} as any)
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			// Consume stream
-			for await (const _ of stream) {
-				// drain
-			}
-
-			expect(mockStreamText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					temperature: 0.6,
-				}),
-			)
-		})
-
-		it("should not pass system prompt to streamText for R1 path", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "response" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
-			})
-
-			vi.spyOn(handler, "getModel").mockReturnValue({
-				id: "some-DeepSeek-R1-model",
-				info: { maxTokens: 2048, temperature: 0.6 },
-				maxTokens: 2048,
-				temperature: 0.6,
-			} as any)
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			for await (const _ of stream) {
-				// drain
-			}
-
-			const callArgs = mockStreamText.mock.calls[0][0]
-			expect(callArgs.system).toBeUndefined()
-			expect(callArgs.messages).toBeDefined()
-		})
-
-		it("should merge consecutive user messages in R1 path to avoid DeepSeek rejection", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "response" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
-			})
-
-			vi.spyOn(handler, "getModel").mockReturnValue({
-				id: "some-DeepSeek-R1-model",
-				info: { maxTokens: 2048, temperature: 0.6 },
-				maxTokens: 2048,
-				temperature: 0.6,
-			} as any)
-
-			// messages starts with a user message, so after prepending the system
-			// prompt as a user message we'd have two consecutive user messages.
-			const userFirstMessages: Anthropic.Messages.MessageParam[] = [
-				{ role: "user", content: "Hello!" },
-				{ role: "assistant", content: "Hi there" },
-				{ role: "user", content: "Follow-up" },
-			]
-
-			const stream = handler.createMessage(systemPrompt, userFirstMessages)
-			for await (const _ of stream) {
-				// drain
-			}
-
-			const callArgs = mockStreamText.mock.calls[0][0]
-			const passedMessages = callArgs.messages
-
-			// Verify no two consecutive messages share the same role
-			for (let i = 1; i < passedMessages.length; i++) {
-				expect(passedMessages[i].role).not.toBe(passedMessages[i - 1].role)
-			}
-
-			// The system prompt and first user message should be merged into a single user message
-			expect(passedMessages[0].role).toBe("user")
-			expect(passedMessages[1].role).toBe("assistant")
-			expect(passedMessages[2].role).toBe("user")
-			expect(passedMessages).toHaveLength(3)
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should complete a prompt using generateText", async () => {
-			mockGenerateText.mockResolvedValue({
-				text: "Test completion from Featherless",
-			})
-
-			const result = await handler.completePrompt("Test prompt")
-
-			expect(result).toBe("Test completion from Featherless")
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					prompt: "Test prompt",
-				}),
-			)
-		})
-	})
-
-	describe("isAiSdkProvider", () => {
-		it("should return true", () => {
-			expect(handler.isAiSdkProvider()).toBe(true)
-		})
-	})
-})

+ 0 - 578
src/api/providers/__tests__/groq.spec.ts

@@ -1,578 +0,0 @@
-// npx vitest run src/api/providers/__tests__/groq.spec.ts
-
-// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
-const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
-	mockStreamText: vi.fn(),
-	mockGenerateText: vi.fn(),
-}))
-
-vi.mock("ai", async (importOriginal) => {
-	const actual = await importOriginal<typeof import("ai")>()
-	return {
-		...actual,
-		streamText: mockStreamText,
-		generateText: mockGenerateText,
-	}
-})
-
-vi.mock("@ai-sdk/groq", () => ({
-	createGroq: vi.fn(() => {
-		// Return a function that returns a mock language model
-		return vi.fn(() => ({
-			modelId: "moonshotai/kimi-k2-instruct-0905",
-			provider: "groq",
-		}))
-	}),
-}))
-
-import type { Anthropic } from "@anthropic-ai/sdk"
-
-import { groqDefaultModelId, groqModels, type GroqModelId } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../../shared/api"
-
-import { GroqHandler } from "../groq"
-
-describe("GroqHandler", () => {
-	let handler: GroqHandler
-	let mockOptions: ApiHandlerOptions
-
-	beforeEach(() => {
-		mockOptions = {
-			groqApiKey: "test-groq-api-key",
-			apiModelId: "moonshotai/kimi-k2-instruct-0905",
-		}
-		handler = new GroqHandler(mockOptions)
-		vi.clearAllMocks()
-	})
-
-	describe("constructor", () => {
-		it("should initialize with provided options", () => {
-			expect(handler).toBeInstanceOf(GroqHandler)
-			expect(handler.getModel().id).toBe(mockOptions.apiModelId)
-		})
-
-		it("should use default model ID if not provided", () => {
-			const handlerWithoutModel = new GroqHandler({
-				...mockOptions,
-				apiModelId: undefined,
-			})
-			expect(handlerWithoutModel.getModel().id).toBe(groqDefaultModelId)
-		})
-	})
-
-	describe("getModel", () => {
-		it("should return default model when no model is specified", () => {
-			const handlerWithoutModel = new GroqHandler({
-				groqApiKey: "test-groq-api-key",
-			})
-			const model = handlerWithoutModel.getModel()
-			expect(model.id).toBe(groqDefaultModelId)
-			expect(model.info).toEqual(groqModels[groqDefaultModelId])
-		})
-
-		it("should return specified model when valid model is provided", () => {
-			const testModelId: GroqModelId = "llama-3.3-70b-versatile"
-			const handlerWithModel = new GroqHandler({
-				apiModelId: testModelId,
-				groqApiKey: "test-groq-api-key",
-			})
-			const model = handlerWithModel.getModel()
-			expect(model.id).toBe(testModelId)
-			expect(model.info).toEqual(groqModels[testModelId])
-		})
-
-		it("should return model info for llama-3.1-8b-instant", () => {
-			const handlerWithLlama = new GroqHandler({
-				...mockOptions,
-				apiModelId: "llama-3.1-8b-instant",
-			})
-			const model = handlerWithLlama.getModel()
-			expect(model.id).toBe("llama-3.1-8b-instant")
-			expect(model.info).toBeDefined()
-			expect(model.info.maxTokens).toBe(8192)
-			expect(model.info.contextWindow).toBe(131072)
-			expect(model.info.supportsImages).toBe(false)
-			expect(model.info.supportsPromptCache).toBe(false)
-		})
-
-		it("should return model info for kimi-k2 which supports prompt cache", () => {
-			const handlerWithKimi = new GroqHandler({
-				...mockOptions,
-				apiModelId: "moonshotai/kimi-k2-instruct-0905",
-			})
-			const model = handlerWithKimi.getModel()
-			expect(model.id).toBe("moonshotai/kimi-k2-instruct-0905")
-			expect(model.info).toBeDefined()
-			expect(model.info.maxTokens).toBe(16384)
-			expect(model.info.contextWindow).toBe(262144)
-			expect(model.info.supportsPromptCache).toBe(true)
-		})
-
-		it("should return provided model ID with default model info if model does not exist", () => {
-			const handlerWithInvalidModel = new GroqHandler({
-				...mockOptions,
-				apiModelId: "invalid-model",
-			})
-			const model = handlerWithInvalidModel.getModel()
-			expect(model.id).toBe("invalid-model")
-			expect(model.info).toBeDefined()
-			// Should use default model info
-			expect(model.info).toBe(groqModels[groqDefaultModelId])
-		})
-
-		it("should include model parameters from getModelParams", () => {
-			const model = handler.getModel()
-			expect(model).toHaveProperty("temperature")
-			expect(model).toHaveProperty("maxTokens")
-		})
-	})
-
-	describe("createMessage", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [
-					{
-						type: "text" as const,
-						text: "Hello!",
-					},
-				],
-			},
-		]
-
-		it("should handle streaming responses", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response from Groq" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks.length).toBeGreaterThan(0)
-			const textChunks = chunks.filter((chunk) => chunk.type === "text")
-			expect(textChunks).toHaveLength(1)
-			expect(textChunks[0].text).toBe("Test response from Groq")
-		})
-
-		it("should include usage information", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 20,
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].inputTokens).toBe(10)
-			expect(usageChunks[0].outputTokens).toBe(20)
-		})
-
-		it("should handle cached tokens in usage data from providerMetadata", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 100,
-				outputTokens: 50,
-			})
-
-			// Groq provides cache metrics via providerMetadata for supported models
-			const mockProviderMetadata = Promise.resolve({
-				groq: {
-					promptCacheHitTokens: 30,
-					promptCacheMissTokens: 70,
-				},
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].inputTokens).toBe(100)
-			expect(usageChunks[0].outputTokens).toBe(50)
-			expect(usageChunks[0].cacheReadTokens).toBe(30)
-			expect(usageChunks[0].cacheWriteTokens).toBe(70)
-		})
-
-		it("should handle usage with details.cachedInputTokens when providerMetadata is not available", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 100,
-				outputTokens: 50,
-				details: {
-					cachedInputTokens: 25,
-				},
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].cacheReadTokens).toBe(25)
-			expect(usageChunks[0].cacheWriteTokens).toBeUndefined()
-		})
-
-		it("should pass correct temperature (0.5 default) to streamText", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
-				providerMetadata: Promise.resolve({}),
-			})
-
-			const handlerWithDefaultTemp = new GroqHandler({
-				groqApiKey: "test-key",
-				apiModelId: "llama-3.1-8b-instant",
-			})
-
-			const stream = handlerWithDefaultTemp.createMessage(systemPrompt, messages)
-			for await (const _ of stream) {
-				// consume stream
-			}
-
-			expect(mockStreamText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					temperature: 0.5,
-				}),
-			)
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should complete a prompt using generateText", async () => {
-			mockGenerateText.mockResolvedValue({
-				text: "Test completion from Groq",
-			})
-
-			const result = await handler.completePrompt("Test prompt")
-
-			expect(result).toBe("Test completion from Groq")
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					prompt: "Test prompt",
-				}),
-			)
-		})
-
-		it("should use default temperature in completePrompt", async () => {
-			mockGenerateText.mockResolvedValue({
-				text: "Test completion",
-			})
-
-			await handler.completePrompt("Test prompt")
-
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					temperature: 0.5,
-				}),
-			)
-		})
-	})
-
-	describe("processUsageMetrics", () => {
-		it("should correctly process usage metrics including cache information from providerMetadata", () => {
-			class TestGroqHandler extends GroqHandler {
-				public testProcessUsageMetrics(usage: any, providerMetadata?: any) {
-					return this.processUsageMetrics(usage, providerMetadata)
-				}
-			}
-
-			const testHandler = new TestGroqHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-			}
-
-			const providerMetadata = {
-				groq: {
-					promptCacheHitTokens: 20,
-					promptCacheMissTokens: 80,
-				},
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage, providerMetadata)
-
-			expect(result.type).toBe("usage")
-			expect(result.inputTokens).toBe(100)
-			expect(result.outputTokens).toBe(50)
-			expect(result.cacheWriteTokens).toBe(80)
-			expect(result.cacheReadTokens).toBe(20)
-		})
-
-		it("should handle missing cache metrics gracefully", () => {
-			class TestGroqHandler extends GroqHandler {
-				public testProcessUsageMetrics(usage: any, providerMetadata?: any) {
-					return this.processUsageMetrics(usage, providerMetadata)
-				}
-			}
-
-			const testHandler = new TestGroqHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage)
-
-			expect(result.type).toBe("usage")
-			expect(result.inputTokens).toBe(100)
-			expect(result.outputTokens).toBe(50)
-			expect(result.cacheWriteTokens).toBeUndefined()
-			expect(result.cacheReadTokens).toBeUndefined()
-		})
-
-		it("should include reasoning tokens when provided", () => {
-			class TestGroqHandler extends GroqHandler {
-				public testProcessUsageMetrics(usage: any, providerMetadata?: any) {
-					return this.processUsageMetrics(usage, providerMetadata)
-				}
-			}
-
-			const testHandler = new TestGroqHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-				details: {
-					reasoningTokens: 30,
-				},
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage)
-
-			expect(result.reasoningTokens).toBe(30)
-		})
-	})
-
-	describe("tool handling", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [{ type: "text" as const, text: "Hello!" }],
-			},
-		]
-
-		it("should handle tool calls in streaming", async () => {
-			async function* mockFullStream() {
-				yield {
-					type: "tool-input-start",
-					id: "tool-call-1",
-					toolName: "read_file",
-				}
-				yield {
-					type: "tool-input-delta",
-					id: "tool-call-1",
-					delta: '{"path":"test.ts"}',
-				}
-				yield {
-					type: "tool-input-end",
-					id: "tool-call-1",
-				}
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages, {
-				taskId: "test-task",
-				tools: [
-					{
-						type: "function",
-						function: {
-							name: "read_file",
-							description: "Read a file",
-							parameters: {
-								type: "object",
-								properties: { path: { type: "string" } },
-								required: ["path"],
-							},
-						},
-					},
-				],
-			})
-
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const toolCallStartChunks = chunks.filter((c) => c.type === "tool_call_start")
-			const toolCallDeltaChunks = chunks.filter((c) => c.type === "tool_call_delta")
-			const toolCallEndChunks = chunks.filter((c) => c.type === "tool_call_end")
-
-			expect(toolCallStartChunks.length).toBe(1)
-			expect(toolCallStartChunks[0].id).toBe("tool-call-1")
-			expect(toolCallStartChunks[0].name).toBe("read_file")
-
-			expect(toolCallDeltaChunks.length).toBe(1)
-			expect(toolCallDeltaChunks[0].delta).toBe('{"path":"test.ts"}')
-
-			expect(toolCallEndChunks.length).toBe(1)
-			expect(toolCallEndChunks[0].id).toBe("tool-call-1")
-		})
-
-		it("should ignore tool-call events to prevent duplicate tools in UI", async () => {
-			async function* mockFullStream() {
-				yield {
-					type: "tool-call",
-					toolCallId: "tool-call-1",
-					toolName: "read_file",
-					input: { path: "test.ts" },
-				}
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages, {
-				taskId: "test-task",
-				tools: [
-					{
-						type: "function",
-						function: {
-							name: "read_file",
-							description: "Read a file",
-							parameters: {
-								type: "object",
-								properties: { path: { type: "string" } },
-								required: ["path"],
-							},
-						},
-					},
-				],
-			})
-
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			// tool-call events are ignored, so no tool_call chunks should be emitted
-			const toolCallChunks = chunks.filter((c) => c.type === "tool_call")
-			expect(toolCallChunks.length).toBe(0)
-		})
-	})
-
-	describe("getMaxOutputTokens", () => {
-		it("should return maxTokens from model info", () => {
-			class TestGroqHandler extends GroqHandler {
-				public testGetMaxOutputTokens() {
-					return this.getMaxOutputTokens()
-				}
-			}
-
-			const testHandler = new TestGroqHandler({
-				...mockOptions,
-				apiModelId: "llama-3.1-8b-instant",
-			})
-			const result = testHandler.testGetMaxOutputTokens()
-
-			// llama-3.1-8b-instant has maxTokens of 8192
-			expect(result).toBe(8192)
-		})
-
-		it("should use modelMaxTokens when provided", () => {
-			class TestGroqHandler extends GroqHandler {
-				public testGetMaxOutputTokens() {
-					return this.getMaxOutputTokens()
-				}
-			}
-
-			const customMaxTokens = 5000
-			const testHandler = new TestGroqHandler({
-				...mockOptions,
-				modelMaxTokens: customMaxTokens,
-			})
-
-			const result = testHandler.testGetMaxOutputTokens()
-			expect(result).toBe(customMaxTokens)
-		})
-	})
-})

+ 0 - 553
src/api/providers/__tests__/huggingface.spec.ts

@@ -1,553 +0,0 @@
-// npx vitest run src/api/providers/__tests__/huggingface.spec.ts
-
-// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
-const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
-	mockStreamText: vi.fn(),
-	mockGenerateText: vi.fn(),
-}))
-
-vi.mock("ai", async (importOriginal) => {
-	const actual = await importOriginal<typeof import("ai")>()
-	return {
-		...actual,
-		streamText: mockStreamText,
-		generateText: mockGenerateText,
-	}
-})
-
-vi.mock("@ai-sdk/openai-compatible", () => ({
-	createOpenAICompatible: vi.fn(() => {
-		// Return a function that returns a mock language model
-		return vi.fn(() => ({
-			modelId: "meta-llama/Llama-3.3-70B-Instruct",
-			provider: "huggingface",
-		}))
-	}),
-}))
-
-// Mock the fetchers
-vi.mock("../fetchers/huggingface", () => ({
-	getHuggingFaceModels: vi.fn(() => Promise.resolve({})),
-	getCachedHuggingFaceModels: vi.fn(() => ({})),
-}))
-
-import type { Anthropic } from "@anthropic-ai/sdk"
-
-import type { ApiHandlerOptions } from "../../../shared/api"
-
-import { HuggingFaceHandler } from "../huggingface"
-
-describe("HuggingFaceHandler", () => {
-	let handler: HuggingFaceHandler
-	let mockOptions: ApiHandlerOptions
-
-	beforeEach(() => {
-		mockOptions = {
-			huggingFaceApiKey: "test-huggingface-api-key",
-			huggingFaceModelId: "meta-llama/Llama-3.3-70B-Instruct",
-		}
-		handler = new HuggingFaceHandler(mockOptions)
-		vi.clearAllMocks()
-	})
-
-	describe("constructor", () => {
-		it("should initialize with provided options", () => {
-			expect(handler).toBeInstanceOf(HuggingFaceHandler)
-			expect(handler.getModel().id).toBe(mockOptions.huggingFaceModelId)
-		})
-
-		it("should use default model ID if not provided", () => {
-			const handlerWithoutModel = new HuggingFaceHandler({
-				...mockOptions,
-				huggingFaceModelId: undefined,
-			})
-			expect(handlerWithoutModel.getModel().id).toBe("meta-llama/Llama-3.3-70B-Instruct")
-		})
-
-		it("should throw error if API key is not provided", () => {
-			expect(() => {
-				new HuggingFaceHandler({
-					...mockOptions,
-					huggingFaceApiKey: undefined,
-				})
-			}).toThrow("Hugging Face API key is required")
-		})
-	})
-
-	describe("getModel", () => {
-		it("should return default model when no model is specified", () => {
-			const handlerWithoutModel = new HuggingFaceHandler({
-				huggingFaceApiKey: "test-huggingface-api-key",
-			})
-			const model = handlerWithoutModel.getModel()
-			expect(model.id).toBe("meta-llama/Llama-3.3-70B-Instruct")
-			expect(model.info).toBeDefined()
-		})
-
-		it("should return specified model when valid model is provided", () => {
-			const testModelId = "mistralai/Mistral-7B-Instruct-v0.3"
-			const handlerWithModel = new HuggingFaceHandler({
-				huggingFaceModelId: testModelId,
-				huggingFaceApiKey: "test-huggingface-api-key",
-			})
-			const model = handlerWithModel.getModel()
-			expect(model.id).toBe(testModelId)
-		})
-
-		it("should include model parameters from getModelParams", () => {
-			const model = handler.getModel()
-			expect(model).toHaveProperty("temperature")
-			expect(model).toHaveProperty("maxTokens")
-		})
-
-		it("should return fallback info when model not in cache", () => {
-			const model = handler.getModel()
-			expect(model.info).toEqual(
-				expect.objectContaining({
-					maxTokens: 8192,
-					contextWindow: 131072,
-					supportsImages: false,
-					supportsPromptCache: false,
-				}),
-			)
-		})
-	})
-
-	describe("createMessage", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [
-					{
-						type: "text" as const,
-						text: "Hello!",
-					},
-				],
-			},
-		]
-
-		it("should handle streaming responses", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response from HuggingFace" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks.length).toBeGreaterThan(0)
-			const textChunks = chunks.filter((chunk) => chunk.type === "text")
-			expect(textChunks).toHaveLength(1)
-			expect(textChunks[0].text).toBe("Test response from HuggingFace")
-		})
-
-		it("should include usage information", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 20,
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].inputTokens).toBe(10)
-			expect(usageChunks[0].outputTokens).toBe(20)
-		})
-
-		it("should handle cached tokens in usage data from providerMetadata", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 100,
-				outputTokens: 50,
-			})
-
-			// HuggingFace provides cache metrics via providerMetadata for supported models
-			const mockProviderMetadata = Promise.resolve({
-				huggingface: {
-					promptCacheHitTokens: 30,
-					promptCacheMissTokens: 70,
-				},
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].inputTokens).toBe(100)
-			expect(usageChunks[0].outputTokens).toBe(50)
-			expect(usageChunks[0].cacheReadTokens).toBe(30)
-			expect(usageChunks[0].cacheWriteTokens).toBe(70)
-		})
-
-		it("should handle usage with details.cachedInputTokens when providerMetadata is not available", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 100,
-				outputTokens: 50,
-				details: {
-					cachedInputTokens: 25,
-				},
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].cacheReadTokens).toBe(25)
-			expect(usageChunks[0].cacheWriteTokens).toBeUndefined()
-		})
-
-		it("should pass correct temperature (0.7 default) to streamText", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
-				providerMetadata: Promise.resolve({}),
-			})
-
-			const handlerWithDefaultTemp = new HuggingFaceHandler({
-				huggingFaceApiKey: "test-key",
-				huggingFaceModelId: "meta-llama/Llama-3.3-70B-Instruct",
-			})
-
-			const stream = handlerWithDefaultTemp.createMessage(systemPrompt, messages)
-			for await (const _ of stream) {
-				// consume stream
-			}
-
-			expect(mockStreamText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					temperature: 0.7,
-				}),
-			)
-		})
-
-		it("should use user-specified temperature over provider defaults", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
-				providerMetadata: Promise.resolve({}),
-			})
-
-			const handlerWithCustomTemp = new HuggingFaceHandler({
-				huggingFaceApiKey: "test-key",
-				huggingFaceModelId: "meta-llama/Llama-3.3-70B-Instruct",
-				modelTemperature: 0.7,
-			})
-
-			const stream = handlerWithCustomTemp.createMessage(systemPrompt, messages)
-			for await (const _ of stream) {
-				// consume stream
-			}
-
-			// User-specified temperature should take precedence over everything
-			expect(mockStreamText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					temperature: 0.7,
-				}),
-			)
-		})
-
-		it("should handle stream with multiple chunks", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Hello" }
-				yield { type: "text-delta", text: " world" }
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 5, outputTokens: 10 }),
-				providerMetadata: Promise.resolve({}),
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const textChunks = chunks.filter((c) => c.type === "text")
-			expect(textChunks[0]).toEqual({ type: "text", text: "Hello" })
-			expect(textChunks[1]).toEqual({ type: "text", text: " world" })
-
-			const usageChunks = chunks.filter((c) => c.type === "usage")
-			expect(usageChunks[0]).toMatchObject({ type: "usage", inputTokens: 5, outputTokens: 10 })
-		})
-
-		it("should handle errors with handleAiSdkError", async () => {
-			async function* mockFullStream(): AsyncGenerator<any> {
-				yield { type: "text-delta", text: "" } // Yield something before error to satisfy lint
-				throw new Error("API Error")
-			}
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
-				providerMetadata: Promise.resolve({}),
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-
-			await expect(async () => {
-				for await (const _ of stream) {
-					// consume stream
-				}
-			}).rejects.toThrow("HuggingFace: API Error")
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should complete a prompt using generateText", async () => {
-			mockGenerateText.mockResolvedValue({
-				text: "Test completion from HuggingFace",
-			})
-
-			const result = await handler.completePrompt("Test prompt")
-
-			expect(result).toBe("Test completion from HuggingFace")
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					prompt: "Test prompt",
-				}),
-			)
-		})
-
-		it("should use default temperature in completePrompt", async () => {
-			mockGenerateText.mockResolvedValue({
-				text: "Test completion",
-			})
-
-			await handler.completePrompt("Test prompt")
-
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					temperature: 0.7,
-				}),
-			)
-		})
-	})
-
-	describe("processUsageMetrics", () => {
-		it("should correctly process usage metrics including cache information from providerMetadata", () => {
-			class TestHuggingFaceHandler extends HuggingFaceHandler {
-				public testProcessUsageMetrics(usage: any, providerMetadata?: any) {
-					return this.processUsageMetrics(usage, providerMetadata)
-				}
-			}
-
-			const testHandler = new TestHuggingFaceHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-			}
-
-			const providerMetadata = {
-				huggingface: {
-					promptCacheHitTokens: 20,
-					promptCacheMissTokens: 80,
-				},
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage, providerMetadata)
-
-			expect(result.type).toBe("usage")
-			expect(result.inputTokens).toBe(100)
-			expect(result.outputTokens).toBe(50)
-			expect(result.cacheWriteTokens).toBe(80)
-			expect(result.cacheReadTokens).toBe(20)
-		})
-
-		it("should handle missing cache metrics gracefully", () => {
-			class TestHuggingFaceHandler extends HuggingFaceHandler {
-				public testProcessUsageMetrics(usage: any, providerMetadata?: any) {
-					return this.processUsageMetrics(usage, providerMetadata)
-				}
-			}
-
-			const testHandler = new TestHuggingFaceHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage)
-
-			expect(result.type).toBe("usage")
-			expect(result.inputTokens).toBe(100)
-			expect(result.outputTokens).toBe(50)
-			expect(result.cacheWriteTokens).toBeUndefined()
-			expect(result.cacheReadTokens).toBeUndefined()
-		})
-
-		it("should include reasoning tokens when provided", () => {
-			class TestHuggingFaceHandler extends HuggingFaceHandler {
-				public testProcessUsageMetrics(usage: any, providerMetadata?: any) {
-					return this.processUsageMetrics(usage, providerMetadata)
-				}
-			}
-
-			const testHandler = new TestHuggingFaceHandler(mockOptions)
-
-			const usage = {
-				inputTokens: 100,
-				outputTokens: 50,
-				details: {
-					reasoningTokens: 30,
-				},
-			}
-
-			const result = testHandler.testProcessUsageMetrics(usage)
-
-			expect(result.reasoningTokens).toBe(30)
-		})
-	})
-
-	describe("tool handling", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [{ type: "text" as const, text: "Hello!" }],
-			},
-		]
-
-		it("should handle tool calls in streaming", async () => {
-			async function* mockFullStream() {
-				yield {
-					type: "tool-input-start",
-					id: "tool-call-1",
-					toolName: "read_file",
-				}
-				yield {
-					type: "tool-input-delta",
-					id: "tool-call-1",
-					delta: '{"path":"test.ts"}',
-				}
-				yield {
-					type: "tool-input-end",
-					id: "tool-call-1",
-				}
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-			})
-
-			const mockProviderMetadata = Promise.resolve({})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-				providerMetadata: mockProviderMetadata,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages, {
-				taskId: "test-task",
-				tools: [
-					{
-						type: "function",
-						function: {
-							name: "read_file",
-							description: "Read a file",
-							parameters: {
-								type: "object",
-								properties: { path: { type: "string" } },
-								required: ["path"],
-							},
-						},
-					},
-				],
-			})
-
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const toolCallStartChunks = chunks.filter((c) => c.type === "tool_call_start")
-			const toolCallDeltaChunks = chunks.filter((c) => c.type === "tool_call_delta")
-			const toolCallEndChunks = chunks.filter((c) => c.type === "tool_call_end")
-
-			expect(toolCallStartChunks.length).toBe(1)
-			expect(toolCallStartChunks[0].id).toBe("tool-call-1")
-			expect(toolCallStartChunks[0].name).toBe("read_file")
-
-			expect(toolCallDeltaChunks.length).toBe(1)
-			expect(toolCallDeltaChunks[0].delta).toBe('{"path":"test.ts"}')
-
-			expect(toolCallEndChunks.length).toBe(1)
-			expect(toolCallEndChunks[0].id).toBe("tool-call-1")
-		})
-	})
-})

+ 0 - 197
src/api/providers/__tests__/io-intelligence.spec.ts

@@ -1,197 +0,0 @@
-const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
-	mockStreamText: vi.fn(),
-	mockGenerateText: vi.fn(),
-}))
-
-vi.mock("ai", async (importOriginal) => {
-	const actual = await importOriginal<typeof import("ai")>()
-	return {
-		...actual,
-		streamText: mockStreamText,
-		generateText: mockGenerateText,
-	}
-})
-
-vi.mock("@ai-sdk/openai-compatible", () => ({
-	createOpenAICompatible: vi.fn(() => {
-		return vi.fn(() => ({
-			modelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
-			provider: "IO Intelligence",
-		}))
-	}),
-}))
-
-import type { Anthropic } from "@anthropic-ai/sdk"
-
-import { ioIntelligenceDefaultModelId } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../../shared/api"
-
-import { IOIntelligenceHandler } from "../io-intelligence"
-
-describe("IOIntelligenceHandler", () => {
-	let handler: IOIntelligenceHandler
-	let mockOptions: ApiHandlerOptions
-
-	beforeEach(() => {
-		mockOptions = {
-			ioIntelligenceApiKey: "test-api-key",
-			ioIntelligenceModelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
-			modelTemperature: 0.7,
-			modelMaxTokens: undefined,
-		} as ApiHandlerOptions
-		handler = new IOIntelligenceHandler(mockOptions)
-		vi.clearAllMocks()
-	})
-
-	describe("constructor", () => {
-		it("should initialize with provided options", () => {
-			expect(handler).toBeInstanceOf(IOIntelligenceHandler)
-			expect(handler.getModel().id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8")
-		})
-
-		it("should use default model ID if not provided", () => {
-			const handlerWithoutModel = new IOIntelligenceHandler({
-				...mockOptions,
-				ioIntelligenceModelId: undefined,
-			} as ApiHandlerOptions)
-			expect(handlerWithoutModel.getModel().id).toBe(ioIntelligenceDefaultModelId)
-		})
-
-		it("should throw error when API key is missing", () => {
-			const optionsWithoutKey = { ...mockOptions }
-			delete optionsWithoutKey.ioIntelligenceApiKey
-
-			expect(() => new IOIntelligenceHandler(optionsWithoutKey)).toThrow("IO Intelligence API key is required")
-		})
-	})
-
-	describe("getModel", () => {
-		it("should return model info for valid model ID", () => {
-			const model = handler.getModel()
-			expect(model.id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8")
-			expect(model.info).toBeDefined()
-			expect(model.info.maxTokens).toBe(8192)
-			expect(model.info.contextWindow).toBe(430000)
-			expect(model.info.supportsImages).toBe(true)
-			expect(model.info.supportsPromptCache).toBe(false)
-		})
-
-		it("should return default model info for unknown model ID", () => {
-			const handlerWithUnknown = new IOIntelligenceHandler({
-				...mockOptions,
-				ioIntelligenceModelId: "unknown-model",
-			} as ApiHandlerOptions)
-			const model = handlerWithUnknown.getModel()
-			expect(model.id).toBe("unknown-model")
-			expect(model.info).toBeDefined()
-			expect(model.info.contextWindow).toBe(handler.getModel().info.contextWindow)
-		})
-
-		it("should return default model if no model ID is provided", () => {
-			const handlerWithoutModel = new IOIntelligenceHandler({
-				...mockOptions,
-				ioIntelligenceModelId: undefined,
-			} as ApiHandlerOptions)
-			const model = handlerWithoutModel.getModel()
-			expect(model.id).toBe(ioIntelligenceDefaultModelId)
-			expect(model.info).toBeDefined()
-		})
-
-		it("should include model parameters from getModelParams", () => {
-			const model = handler.getModel()
-			expect(model).toHaveProperty("temperature")
-			expect(model).toHaveProperty("maxTokens")
-		})
-	})
-
-	describe("createMessage", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: [
-					{
-						type: "text" as const,
-						text: "Hello!",
-					},
-				],
-			},
-		]
-
-		it("should handle streaming responses", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-				details: {},
-				raw: {},
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks.length).toBeGreaterThan(0)
-			const textChunks = chunks.filter((chunk) => chunk.type === "text")
-			expect(textChunks).toHaveLength(1)
-			expect(textChunks[0].text).toBe("Test response")
-		})
-
-		it("should include usage information", async () => {
-			async function* mockFullStream() {
-				yield { type: "text-delta", text: "Test response" }
-			}
-
-			const mockUsage = Promise.resolve({
-				inputTokens: 10,
-				outputTokens: 5,
-				details: {},
-				raw: {},
-			})
-
-			mockStreamText.mockReturnValue({
-				fullStream: mockFullStream(),
-				usage: mockUsage,
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: any[] = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
-			expect(usageChunks.length).toBeGreaterThan(0)
-			expect(usageChunks[0].inputTokens).toBe(10)
-			expect(usageChunks[0].outputTokens).toBe(5)
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should complete a prompt using generateText", async () => {
-			mockGenerateText.mockResolvedValue({
-				text: "Test completion",
-			})
-
-			const result = await handler.completePrompt("Test prompt")
-
-			expect(result).toBe("Test completion")
-			expect(mockGenerateText).toHaveBeenCalledWith(
-				expect.objectContaining({
-					prompt: "Test prompt",
-				}),
-			)
-		})
-	})
-})

+ 0 - 549
src/api/providers/__tests__/unbound.spec.ts

@@ -1,549 +0,0 @@
-// npx vitest run src/api/providers/__tests__/unbound.spec.ts
-
-import { Anthropic } from "@anthropic-ai/sdk"
-
-import { ApiHandlerOptions } from "../../../shared/api"
-
-import { UnboundHandler } from "../unbound"
-
-// Mock dependencies
-vitest.mock("../fetchers/modelCache", () => ({
-	getModels: vitest.fn().mockImplementation(() => {
-		return Promise.resolve({
-			"anthropic/claude-3-5-sonnet-20241022": {
-				maxTokens: 8192,
-				contextWindow: 200000,
-				supportsImages: true,
-				supportsPromptCache: true,
-				inputPrice: 3,
-				outputPrice: 15,
-				cacheWritesPrice: 3.75,
-				cacheReadsPrice: 0.3,
-				description: "Claude 3.5 Sonnet",
-				thinking: false,
-			},
-			"anthropic/claude-sonnet-4-5": {
-				maxTokens: 8192,
-				contextWindow: 200000,
-				supportsImages: true,
-				supportsPromptCache: true,
-				inputPrice: 3,
-				outputPrice: 15,
-				cacheWritesPrice: 3.75,
-				cacheReadsPrice: 0.3,
-				description: "Claude 4.5 Sonnet",
-				thinking: false,
-			},
-			"anthropic/claude-3-7-sonnet-20250219": {
-				maxTokens: 8192,
-				contextWindow: 200000,
-				supportsImages: true,
-				supportsPromptCache: true,
-				inputPrice: 3,
-				outputPrice: 15,
-				cacheWritesPrice: 3.75,
-				cacheReadsPrice: 0.3,
-				description: "Claude 3.7 Sonnet",
-				thinking: false,
-			},
-			"openai/gpt-4o": {
-				maxTokens: 4096,
-				contextWindow: 128000,
-				supportsImages: true,
-				supportsPromptCache: false,
-				inputPrice: 5,
-				outputPrice: 15,
-				description: "GPT-4o",
-			},
-			"openai/o3-mini": {
-				maxTokens: 4096,
-				contextWindow: 128000,
-				supportsImages: true,
-				supportsPromptCache: false,
-				inputPrice: 1,
-				outputPrice: 3,
-				description: "O3 Mini",
-			},
-		})
-	}),
-	getModelsFromCache: vitest.fn().mockReturnValue(undefined),
-}))
-
-// Mock OpenAI client
-const mockCreate = vitest.fn()
-const mockWithResponse = vitest.fn()
-
-vitest.mock("openai", () => {
-	return {
-		__esModule: true,
-		default: vitest.fn().mockImplementation(() => ({
-			chat: {
-				completions: {
-					create: (...args: any[]) => {
-						const stream = {
-							[Symbol.asyncIterator]: async function* () {
-								// First chunk with content
-								yield {
-									choices: [{ delta: { content: "Test response" }, index: 0 }],
-								}
-								// Second chunk with usage data
-								yield {
-									choices: [{ delta: {}, index: 0 }],
-									usage: {
-										prompt_tokens: 10,
-										completion_tokens: 5,
-										total_tokens: 15,
-									},
-								}
-								// Third chunk with cache usage data
-								yield {
-									choices: [{ delta: {}, index: 0 }],
-									usage: {
-										prompt_tokens: 8,
-										completion_tokens: 4,
-										total_tokens: 12,
-										cache_creation_input_tokens: 3,
-										cache_read_input_tokens: 2,
-									},
-								}
-							},
-						}
-
-						const result = mockCreate(...args)
-
-						if (args[0].stream) {
-							mockWithResponse.mockReturnValue(
-								Promise.resolve({ data: stream, response: { headers: new Map() } }),
-							)
-							result.withResponse = mockWithResponse
-						}
-
-						return result
-					},
-				},
-			},
-		})),
-	}
-})
-
-describe("UnboundHandler", () => {
-	let handler: UnboundHandler
-	let mockOptions: ApiHandlerOptions
-
-	beforeEach(() => {
-		mockOptions = {
-			unboundApiKey: "test-api-key",
-			unboundModelId: "anthropic/claude-3-5-sonnet-20241022",
-		}
-
-		handler = new UnboundHandler(mockOptions)
-		mockCreate.mockClear()
-		mockWithResponse.mockClear()
-
-		// Default mock implementation for non-streaming responses
-		mockCreate.mockResolvedValue({
-			id: "test-completion",
-			choices: [
-				{
-					message: { role: "assistant", content: "Test response" },
-					finish_reason: "stop",
-					index: 0,
-				},
-			],
-		})
-	})
-
-	describe("constructor", () => {
-		it("should initialize with provided options", async () => {
-			expect(handler).toBeInstanceOf(UnboundHandler)
-			expect((await handler.fetchModel()).id).toBe(mockOptions.unboundModelId)
-		})
-	})
-
-	describe("createMessage", () => {
-		const systemPrompt = "You are a helpful assistant."
-		const messages: Anthropic.Messages.MessageParam[] = [
-			{
-				role: "user",
-				content: "Hello!",
-			},
-		]
-
-		it("should handle streaming responses with text and usage data", async () => {
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks: Array<{ type: string } & Record<string, any>> = []
-
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks.length).toBe(3)
-
-			// Verify text chunk
-			expect(chunks[0]).toEqual({ type: "text", text: "Test response" })
-
-			// Verify regular usage data
-			expect(chunks[1]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 5 })
-
-			// Verify usage data with cache information
-			expect(chunks[2]).toEqual({
-				type: "usage",
-				inputTokens: 8,
-				outputTokens: 4,
-				cacheWriteTokens: 3,
-				cacheReadTokens: 2,
-			})
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					model: "claude-3-5-sonnet-20241022",
-					messages: expect.any(Array),
-					stream: true,
-				}),
-
-				expect.objectContaining({
-					headers: {
-						"X-Unbound-Metadata": expect.stringContaining("roo-code"),
-					},
-				}),
-			)
-		})
-
-		it("should handle API errors", async () => {
-			mockCreate.mockImplementationOnce(() => {
-				throw new Error("API Error")
-			})
-
-			const stream = handler.createMessage(systemPrompt, messages)
-			const chunks = []
-
-			try {
-				for await (const chunk of stream) {
-					chunks.push(chunk)
-				}
-
-				expect.fail("Expected error to be thrown")
-			} catch (error) {
-				expect(error).toBeInstanceOf(Error)
-				expect(error.message).toBe("API Error")
-			}
-		})
-	})
-
-	describe("completePrompt", () => {
-		it("should complete prompt successfully", async () => {
-			const result = await handler.completePrompt("Test prompt")
-			expect(result).toBe("Test response")
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					model: "claude-3-5-sonnet-20241022",
-					messages: [{ role: "user", content: "Test prompt" }],
-					temperature: 0,
-					max_tokens: 8192,
-				}),
-				expect.objectContaining({
-					headers: expect.objectContaining({
-						"X-Unbound-Metadata": expect.stringContaining("roo-code"),
-					}),
-				}),
-			)
-		})
-
-		it("should handle API errors", async () => {
-			mockCreate.mockRejectedValueOnce(new Error("API Error"))
-			await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Unbound completion error: API Error")
-		})
-
-		it("should handle empty response", async () => {
-			mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: "" } }] })
-			const result = await handler.completePrompt("Test prompt")
-			expect(result).toBe("")
-		})
-
-		it("should not set max_tokens for non-Anthropic models", async () => {
-			mockCreate.mockClear()
-
-			const nonAnthropicHandler = new UnboundHandler({
-				apiModelId: "openai/gpt-4o",
-				unboundApiKey: "test-key",
-				unboundModelId: "openai/gpt-4o",
-			})
-
-			await nonAnthropicHandler.completePrompt("Test prompt")
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					model: "gpt-4o",
-					messages: [{ role: "user", content: "Test prompt" }],
-					temperature: 0,
-				}),
-				expect.objectContaining({
-					headers: expect.objectContaining({
-						"X-Unbound-Metadata": expect.stringContaining("roo-code"),
-					}),
-				}),
-			)
-
-			expect(mockCreate.mock.calls[0][0]).not.toHaveProperty("max_tokens")
-		})
-
-		it("should not set temperature for openai/o3-mini", async () => {
-			mockCreate.mockClear()
-
-			const openaiHandler = new UnboundHandler({
-				apiModelId: "openai/o3-mini",
-				unboundApiKey: "test-key",
-				unboundModelId: "openai/o3-mini",
-			})
-
-			await openaiHandler.completePrompt("Test prompt")
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					model: "o3-mini",
-					messages: [{ role: "user", content: "Test prompt" }],
-				}),
-				expect.objectContaining({
-					headers: expect.objectContaining({
-						"X-Unbound-Metadata": expect.stringContaining("roo-code"),
-					}),
-				}),
-			)
-
-			expect(mockCreate.mock.calls[0][0]).not.toHaveProperty("temperature")
-		})
-	})
-
-	describe("fetchModel", () => {
-		it("should return model info", async () => {
-			const modelInfo = await handler.fetchModel()
-			expect(modelInfo.id).toBe(mockOptions.unboundModelId)
-			expect(modelInfo.info).toBeDefined()
-		})
-
-		it("should return default model when invalid model provided", async () => {
-			const handlerWithInvalidModel = new UnboundHandler({ ...mockOptions, unboundModelId: "invalid/model" })
-			const modelInfo = await handlerWithInvalidModel.fetchModel()
-			expect(modelInfo.id).toBe("anthropic/claude-sonnet-4-5")
-			expect(modelInfo.info).toBeDefined()
-		})
-	})
-
-	describe("Native Tool Calling", () => {
-		const testTools = [
-			{
-				type: "function" as const,
-				function: {
-					name: "test_tool",
-					description: "A test tool",
-					parameters: {
-						type: "object",
-						properties: {
-							arg1: { type: "string", description: "First argument" },
-						},
-						required: ["arg1"],
-					},
-				},
-			},
-		]
-
-		it("should include tools in request when tools are provided", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-			})
-			await messageGenerator.next()
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					tools: expect.arrayContaining([
-						expect.objectContaining({
-							type: "function",
-							function: expect.objectContaining({
-								name: "test_tool",
-							}),
-						}),
-					]),
-					parallel_tool_calls: true,
-				}),
-				expect.objectContaining({
-					headers: {
-						"X-Unbound-Metadata": expect.stringContaining("roo-code"),
-					},
-				}),
-			)
-		})
-
-		it("should include tool_choice when provided", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-				tool_choice: "auto",
-			})
-			await messageGenerator.next()
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					tool_choice: "auto",
-				}),
-				expect.objectContaining({
-					headers: {
-						"X-Unbound-Metadata": expect.stringContaining("roo-code"),
-					},
-				}),
-			)
-		})
-
-		it("should always include tools and tool_choice (tools are guaranteed to be present after ALWAYS_AVAILABLE_TOOLS)", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-			})
-			await messageGenerator.next()
-
-			// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
-			const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
-			expect(callArgs).toHaveProperty("tools")
-			expect(callArgs).toHaveProperty("tool_choice")
-			expect(callArgs).toHaveProperty("parallel_tool_calls", true)
-		})
-
-		it("should yield tool_call_partial chunks during streaming", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						next: vi
-							.fn()
-							.mockResolvedValueOnce({
-								done: false,
-								value: {
-									choices: [
-										{
-											delta: {
-												tool_calls: [
-													{
-														index: 0,
-														id: "call_123",
-														function: {
-															name: "test_tool",
-															arguments: '{"arg1":',
-														},
-													},
-												],
-											},
-										},
-									],
-								},
-							})
-							.mockResolvedValueOnce({
-								done: false,
-								value: {
-									choices: [
-										{
-											delta: {
-												tool_calls: [
-													{
-														index: 0,
-														function: {
-															arguments: '"value"}',
-														},
-													},
-												],
-											},
-										},
-									],
-								},
-							})
-							.mockResolvedValueOnce({ done: true }),
-					}),
-				},
-			})
-
-			const stream = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-			})
-
-			const chunks = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks).toContainEqual({
-				type: "tool_call_partial",
-				index: 0,
-				id: "call_123",
-				name: "test_tool",
-				arguments: '{"arg1":',
-			})
-
-			expect(chunks).toContainEqual({
-				type: "tool_call_partial",
-				index: 0,
-				id: undefined,
-				name: undefined,
-				arguments: '"value"}',
-			})
-		})
-
-		it("should set parallel_tool_calls based on metadata", async () => {
-			mockWithResponse.mockResolvedValueOnce({
-				data: {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				},
-			})
-
-			const messageGenerator = handler.createMessage("test prompt", [], {
-				taskId: "test-task-id",
-				tools: testTools,
-				parallelToolCalls: true,
-			})
-			await messageGenerator.next()
-
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({
-					parallel_tool_calls: true,
-				}),
-				expect.objectContaining({
-					headers: {
-						"X-Unbound-Metadata": expect.stringContaining("roo-code"),
-					},
-				}),
-			)
-		})
-	})
-})

+ 0 - 169
src/api/providers/cerebras.ts

@@ -1,169 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-import { createCerebras } from "@ai-sdk/cerebras"
-import { streamText, generateText, ToolSet } from "ai"
-
-import { cerebrasModels, cerebrasDefaultModelId, type CerebrasModelId, type ModelInfo } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-
-import {
-	convertToAiSdkMessages,
-	convertToolsForAiSdk,
-	processAiSdkStreamPart,
-	mapToolChoice,
-	handleAiSdkError,
-} from "../transform/ai-sdk"
-import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
-import { getModelParams } from "../transform/model-params"
-
-import { DEFAULT_HEADERS } from "./constants"
-import { BaseProvider } from "./base-provider"
-import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
-
-const CEREBRAS_INTEGRATION_HEADER = "X-Cerebras-3rd-Party-Integration"
-const CEREBRAS_INTEGRATION_NAME = "roocode"
-const CEREBRAS_DEFAULT_TEMPERATURE = 0
-
-/**
- * Cerebras provider using the dedicated @ai-sdk/cerebras package.
- * Provides high-speed inference powered by Wafer-Scale Engines.
- */
-export class CerebrasHandler extends BaseProvider implements SingleCompletionHandler {
-	protected options: ApiHandlerOptions
-	protected provider: ReturnType<typeof createCerebras>
-
-	constructor(options: ApiHandlerOptions) {
-		super()
-		this.options = options
-
-		// Create the Cerebras provider using AI SDK
-		this.provider = createCerebras({
-			apiKey: options.cerebrasApiKey ?? "not-provided",
-			headers: {
-				...DEFAULT_HEADERS,
-				[CEREBRAS_INTEGRATION_HEADER]: CEREBRAS_INTEGRATION_NAME,
-			},
-		})
-	}
-
-	override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number } {
-		const id = (this.options.apiModelId ?? cerebrasDefaultModelId) as CerebrasModelId
-		const info = cerebrasModels[id as keyof typeof cerebrasModels] || cerebrasModels[cerebrasDefaultModelId]
-		const params = getModelParams({
-			format: "openai",
-			modelId: id,
-			model: info,
-			settings: this.options,
-			defaultTemperature: CEREBRAS_DEFAULT_TEMPERATURE,
-		})
-		return { id, info, ...params }
-	}
-
-	/**
-	 * Get the language model for the configured model ID.
-	 */
-	protected getLanguageModel() {
-		const { id } = this.getModel()
-		return this.provider(id)
-	}
-
-	/**
-	 * Process usage metrics from the AI SDK response.
-	 */
-	protected processUsageMetrics(usage: {
-		inputTokens?: number
-		outputTokens?: number
-		details?: {
-			cachedInputTokens?: number
-			reasoningTokens?: number
-		}
-	}): ApiStreamUsageChunk {
-		return {
-			type: "usage",
-			inputTokens: usage.inputTokens || 0,
-			outputTokens: usage.outputTokens || 0,
-			cacheReadTokens: usage.details?.cachedInputTokens,
-			reasoningTokens: usage.details?.reasoningTokens,
-		}
-	}
-
-	/**
-	 * Get the max tokens parameter to include in the request.
-	 */
-	protected getMaxOutputTokens(): number | undefined {
-		const { info } = this.getModel()
-		return this.options.modelMaxTokens || info.maxTokens || undefined
-	}
-
-	/**
-	 * Create a message stream using the AI SDK.
-	 */
-	override async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		const { temperature } = this.getModel()
-		const languageModel = this.getLanguageModel()
-
-		// Convert messages to AI SDK format
-		const aiSdkMessages = convertToAiSdkMessages(messages)
-
-		// Convert tools to OpenAI format first, then to AI SDK format
-		const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
-		const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
-
-		// Build the request options
-		const requestOptions: Parameters<typeof streamText>[0] = {
-			model: languageModel,
-			system: systemPrompt,
-			messages: aiSdkMessages,
-			temperature: this.options.modelTemperature ?? temperature ?? CEREBRAS_DEFAULT_TEMPERATURE,
-			maxOutputTokens: this.getMaxOutputTokens(),
-			tools: aiSdkTools,
-			toolChoice: mapToolChoice(metadata?.tool_choice),
-		}
-
-		// Use streamText for streaming responses
-		const result = streamText(requestOptions)
-
-		try {
-			// Process the full stream to get all events including reasoning
-			for await (const part of result.fullStream) {
-				for (const chunk of processAiSdkStreamPart(part)) {
-					yield chunk
-				}
-			}
-
-			// Yield usage metrics at the end
-			const usage = await result.usage
-			if (usage) {
-				yield this.processUsageMetrics(usage)
-			}
-		} catch (error) {
-			// Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.)
-			throw handleAiSdkError(error, "Cerebras")
-		}
-	}
-
-	/**
-	 * Complete a prompt using the AI SDK generateText.
-	 */
-	async completePrompt(prompt: string): Promise<string> {
-		const { temperature } = this.getModel()
-		const languageModel = this.getLanguageModel()
-
-		const { text } = await generateText({
-			model: languageModel,
-			prompt,
-			maxOutputTokens: this.getMaxOutputTokens(),
-			temperature: this.options.modelTemperature ?? temperature ?? CEREBRAS_DEFAULT_TEMPERATURE,
-		})
-
-		return text
-	}
-
-	override isAiSdkProvider(): boolean {
-		return true
-	}
-}

+ 0 - 242
src/api/providers/chutes.ts

@@ -1,242 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-import { streamText, generateText, LanguageModel, ToolSet } from "ai"
-
-import {
-	DEEP_SEEK_DEFAULT_TEMPERATURE,
-	chutesDefaultModelId,
-	chutesDefaultModelInfo,
-	type ModelInfo,
-	type ModelRecord,
-} from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-import { getModelMaxOutputTokens } from "../../shared/api"
-import { TagMatcher } from "../../utils/tag-matcher"
-import {
-	convertToAiSdkMessages,
-	convertToolsForAiSdk,
-	processAiSdkStreamPart,
-	mapToolChoice,
-	handleAiSdkError,
-} from "../transform/ai-sdk"
-import { ApiStream } from "../transform/stream"
-import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
-
-import { OpenAICompatibleHandler, OpenAICompatibleConfig } from "./openai-compatible"
-import { getModels, getModelsFromCache } from "./fetchers/modelCache"
-
-export class ChutesHandler extends OpenAICompatibleHandler implements SingleCompletionHandler {
-	private models: ModelRecord = {}
-
-	constructor(options: ApiHandlerOptions) {
-		const modelId = options.apiModelId ?? chutesDefaultModelId
-
-		const config: OpenAICompatibleConfig = {
-			providerName: "chutes",
-			baseURL: "https://llm.chutes.ai/v1",
-			apiKey: options.chutesApiKey ?? "not-provided",
-			modelId,
-			modelInfo: chutesDefaultModelInfo,
-		}
-
-		super(options, config)
-	}
-
-	async fetchModel() {
-		this.models = await getModels({ provider: "chutes", apiKey: this.config.apiKey, baseUrl: this.config.baseURL })
-		return this.getModel()
-	}
-
-	override getModel(): { id: string; info: ModelInfo; temperature?: number } {
-		const id = this.options.apiModelId ?? chutesDefaultModelId
-
-		let info: ModelInfo | undefined = this.models[id]
-
-		if (!info) {
-			const cachedModels = getModelsFromCache("chutes")
-			if (cachedModels?.[id]) {
-				this.models = cachedModels
-				info = cachedModels[id]
-			}
-		}
-
-		if (!info) {
-			const isDeepSeekR1 = chutesDefaultModelId.includes("DeepSeek-R1")
-			const defaultTemp = isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0.5
-			return {
-				id: chutesDefaultModelId,
-				info: {
-					...chutesDefaultModelInfo,
-					defaultTemperature: defaultTemp,
-				},
-				temperature: this.options.modelTemperature ?? defaultTemp,
-			}
-		}
-
-		const isDeepSeekR1 = id.includes("DeepSeek-R1")
-		const defaultTemp = isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0.5
-
-		return {
-			id,
-			info: {
-				...info,
-				defaultTemperature: defaultTemp,
-			},
-			temperature: this.supportsTemperature(id) ? (this.options.modelTemperature ?? defaultTemp) : undefined,
-		}
-	}
-
-	protected override getLanguageModel(): LanguageModel {
-		const { id } = this.getModel()
-		return this.provider(id)
-	}
-
-	protected override getMaxOutputTokens(): number | undefined {
-		const { id, info } = this.getModel()
-		return (
-			getModelMaxOutputTokens({
-				modelId: id,
-				model: info,
-				settings: this.options,
-				format: "openai",
-			}) ?? undefined
-		)
-	}
-
-	private supportsTemperature(modelId: string): boolean {
-		return !modelId.startsWith("openai/o3-mini")
-	}
-
-	override async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		const model = await this.fetchModel()
-
-		if (model.id.includes("DeepSeek-R1")) {
-			yield* this.createR1Message(systemPrompt, messages, model, metadata)
-		} else {
-			yield* super.createMessage(systemPrompt, messages, metadata)
-		}
-	}
-
-	private async *createR1Message(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		model: { id: string; info: ModelInfo },
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		const languageModel = this.getLanguageModel()
-
-		const modifiedMessages = [...messages] as Anthropic.Messages.MessageParam[]
-
-		if (modifiedMessages.length > 0 && modifiedMessages[0].role === "user") {
-			const first = modifiedMessages[0]
-			if (typeof first.content === "string") {
-				modifiedMessages[0] = { role: "user", content: `${systemPrompt}\n\n${first.content}` }
-			} else {
-				modifiedMessages[0] = {
-					role: "user",
-					content: [{ type: "text", text: systemPrompt }, ...first.content],
-				}
-			}
-		} else {
-			modifiedMessages.unshift({ role: "user", content: systemPrompt })
-		}
-
-		const aiSdkMessages = convertToAiSdkMessages(modifiedMessages)
-
-		const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
-		const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
-
-		const maxOutputTokens =
-			getModelMaxOutputTokens({
-				modelId: model.id,
-				model: model.info,
-				settings: this.options,
-				format: "openai",
-			}) ?? undefined
-
-		const temperature = this.supportsTemperature(model.id)
-			? (this.options.modelTemperature ?? model.info.defaultTemperature)
-			: undefined
-
-		const result = streamText({
-			model: languageModel,
-			messages: aiSdkMessages,
-			temperature,
-			maxOutputTokens,
-			tools: aiSdkTools,
-			toolChoice: mapToolChoice(metadata?.tool_choice),
-		})
-
-		const matcher = new TagMatcher(
-			"think",
-			(chunk) =>
-				({
-					type: chunk.matched ? "reasoning" : "text",
-					text: chunk.data,
-				}) as const,
-		)
-
-		try {
-			for await (const part of result.fullStream) {
-				if (part.type === "text-delta") {
-					for (const processedChunk of matcher.update(part.text)) {
-						yield processedChunk
-					}
-				} else {
-					for (const chunk of processAiSdkStreamPart(part)) {
-						yield chunk
-					}
-				}
-			}
-
-			for (const processedChunk of matcher.final()) {
-				yield processedChunk
-			}
-
-			const usage = await result.usage
-			if (usage) {
-				yield this.processUsageMetrics(usage)
-			}
-		} catch (error) {
-			throw handleAiSdkError(error, "chutes")
-		}
-	}
-
-	override async completePrompt(prompt: string): Promise<string> {
-		const model = await this.fetchModel()
-		const languageModel = this.getLanguageModel()
-
-		const maxOutputTokens =
-			getModelMaxOutputTokens({
-				modelId: model.id,
-				model: model.info,
-				settings: this.options,
-				format: "openai",
-			}) ?? undefined
-
-		const isDeepSeekR1 = model.id.includes("DeepSeek-R1")
-		const defaultTemperature = isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0.5
-		const temperature = this.supportsTemperature(model.id)
-			? (this.options.modelTemperature ?? defaultTemperature)
-			: undefined
-
-		try {
-			const { text } = await generateText({
-				model: languageModel,
-				prompt,
-				maxOutputTokens,
-				temperature,
-			})
-			return text
-		} catch (error) {
-			if (error instanceof Error) {
-				throw new Error(`Chutes completion error: ${error.message}`)
-			}
-			throw error
-		}
-	}
-}

+ 0 - 164
src/api/providers/deepinfra.ts

@@ -1,164 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-import OpenAI from "openai"
-
-import { deepInfraDefaultModelId, deepInfraDefaultModelInfo } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-import { calculateApiCostOpenAI } from "../../shared/cost"
-
-import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
-import { convertToOpenAiMessages } from "../transform/openai-format"
-
-import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
-import { RouterProvider } from "./router-provider"
-import { getModelParams } from "../transform/model-params"
-import { getModels } from "./fetchers/modelCache"
-
-export class DeepInfraHandler extends RouterProvider implements SingleCompletionHandler {
-	constructor(options: ApiHandlerOptions) {
-		super({
-			options: {
-				...options,
-				openAiHeaders: {
-					"X-Deepinfra-Source": "roo-code",
-					"X-Deepinfra-Version": `2025-08-25`,
-				},
-			},
-			name: "deepinfra",
-			baseURL: `${options.deepInfraBaseUrl || "https://api.deepinfra.com/v1/openai"}`,
-			apiKey: options.deepInfraApiKey || "not-provided",
-			modelId: options.deepInfraModelId,
-			defaultModelId: deepInfraDefaultModelId,
-			defaultModelInfo: deepInfraDefaultModelInfo,
-		})
-	}
-
-	public override async fetchModel() {
-		this.models = await getModels({ provider: this.name, apiKey: this.client.apiKey, baseUrl: this.client.baseURL })
-		return this.getModel()
-	}
-
-	override getModel() {
-		const id = this.options.deepInfraModelId ?? deepInfraDefaultModelId
-		const info = this.models[id] ?? deepInfraDefaultModelInfo
-
-		const params = getModelParams({
-			format: "openai",
-			modelId: id,
-			model: info,
-			settings: this.options,
-			defaultTemperature: 0,
-		})
-
-		return { id, info, ...params }
-	}
-
-	override async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		_metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		// Ensure we have up-to-date model metadata
-		await this.fetchModel()
-		const { id: modelId, info, reasoningEffort: reasoning_effort } = await this.fetchModel()
-		let prompt_cache_key = undefined
-		if (info.supportsPromptCache && _metadata?.taskId) {
-			prompt_cache_key = _metadata.taskId
-		}
-
-		const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
-			model: modelId,
-			messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
-			stream: true,
-			stream_options: { include_usage: true },
-			reasoning_effort,
-			prompt_cache_key,
-			tools: this.convertToolsForOpenAI(_metadata?.tools),
-			tool_choice: _metadata?.tool_choice,
-			parallel_tool_calls: _metadata?.parallelToolCalls ?? true,
-		} as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
-
-		if (this.supportsTemperature(modelId)) {
-			requestOptions.temperature = this.options.modelTemperature ?? 0
-		}
-
-		if (this.options.includeMaxTokens === true && info.maxTokens) {
-			;(requestOptions as any).max_completion_tokens = this.options.modelMaxTokens || info.maxTokens
-		}
-
-		const { data: stream } = await this.client.chat.completions.create(requestOptions).withResponse()
-
-		let lastUsage: OpenAI.CompletionUsage | undefined
-		for await (const chunk of stream) {
-			const delta = chunk.choices[0]?.delta
-
-			if (delta?.content) {
-				yield { type: "text", text: delta.content }
-			}
-
-			if (delta && "reasoning_content" in delta && delta.reasoning_content) {
-				yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" }
-			}
-
-			// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
-			if (delta?.tool_calls) {
-				for (const toolCall of delta.tool_calls) {
-					yield {
-						type: "tool_call_partial",
-						index: toolCall.index,
-						id: toolCall.id,
-						name: toolCall.function?.name,
-						arguments: toolCall.function?.arguments,
-					}
-				}
-			}
-
-			if (chunk.usage) {
-				lastUsage = chunk.usage
-			}
-		}
-
-		if (lastUsage) {
-			yield this.processUsageMetrics(lastUsage, info)
-		}
-	}
-
-	async completePrompt(prompt: string): Promise<string> {
-		await this.fetchModel()
-		const { id: modelId, info } = this.getModel()
-
-		const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
-			model: modelId,
-			messages: [{ role: "user", content: prompt }],
-		}
-		if (this.supportsTemperature(modelId)) {
-			requestOptions.temperature = this.options.modelTemperature ?? 0
-		}
-		if (this.options.includeMaxTokens === true && info.maxTokens) {
-			;(requestOptions as any).max_completion_tokens = this.options.modelMaxTokens || info.maxTokens
-		}
-
-		const resp = await this.client.chat.completions.create(requestOptions)
-		return resp.choices[0]?.message?.content || ""
-	}
-
-	protected processUsageMetrics(usage: any, modelInfo?: any): ApiStreamUsageChunk {
-		const inputTokens = usage?.prompt_tokens || 0
-		const outputTokens = usage?.completion_tokens || 0
-		const cacheWriteTokens = usage?.prompt_tokens_details?.cache_write_tokens || 0
-		const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0
-
-		const { totalCost } = modelInfo
-			? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
-			: { totalCost: 0 }
-
-		return {
-			type: "usage",
-			inputTokens,
-			outputTokens,
-			cacheWriteTokens: cacheWriteTokens || undefined,
-			cacheReadTokens: cacheReadTokens || undefined,
-			totalCost,
-		}
-	}
-}

+ 0 - 87
src/api/providers/doubao.ts

@@ -1,87 +0,0 @@
-import { OpenAiHandler } from "./openai"
-import type { ApiHandlerOptions } from "../../shared/api"
-import { DOUBAO_API_BASE_URL, doubaoDefaultModelId, doubaoModels } from "@roo-code/types"
-import { getModelParams } from "../transform/model-params"
-import { ApiStreamUsageChunk } from "../transform/stream"
-
-// Core types for Doubao API
-interface ChatCompletionMessageParam {
-	role: "system" | "user" | "assistant" | "developer"
-	content:
-		| string
-		| Array<{
-				type: "text" | "image_url"
-				text?: string
-				image_url?: { url: string }
-		  }>
-}
-
-interface ChatCompletionParams {
-	model: string
-	messages: ChatCompletionMessageParam[]
-	temperature?: number
-	stream?: boolean
-	stream_options?: { include_usage: boolean }
-	max_completion_tokens?: number
-}
-
-interface ChatCompletion {
-	choices: Array<{
-		message: {
-			content: string
-		}
-	}>
-	usage?: {
-		prompt_tokens: number
-		completion_tokens: number
-	}
-}
-
-interface ChatCompletionChunk {
-	choices: Array<{
-		delta: {
-			content?: string
-		}
-	}>
-	usage?: {
-		prompt_tokens: number
-		completion_tokens: number
-	}
-}
-
-export class DoubaoHandler extends OpenAiHandler {
-	constructor(options: ApiHandlerOptions) {
-		super({
-			...options,
-			openAiApiKey: options.doubaoApiKey ?? "not-provided",
-			openAiModelId: options.apiModelId ?? doubaoDefaultModelId,
-			openAiBaseUrl: options.doubaoBaseUrl ?? DOUBAO_API_BASE_URL,
-			openAiStreamingEnabled: true,
-			includeMaxTokens: true,
-		})
-	}
-
-	override getModel() {
-		const id = this.options.apiModelId ?? doubaoDefaultModelId
-		const info = doubaoModels[id as keyof typeof doubaoModels] || doubaoModels[doubaoDefaultModelId]
-		const params = getModelParams({
-			format: "openai",
-			modelId: id,
-			model: info,
-			settings: this.options,
-			defaultTemperature: 0,
-		})
-		return { id, info, ...params }
-	}
-
-	// Override to handle Doubao's usage metrics, including caching.
-	protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
-		return {
-			type: "usage",
-			inputTokens: usage?.prompt_tokens || 0,
-			outputTokens: usage?.completion_tokens || 0,
-			cacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,
-			cacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,
-		}
-	}
-}

+ 0 - 140
src/api/providers/featherless.ts

@@ -1,140 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-import { streamText } from "ai"
-
-import { DEEP_SEEK_DEFAULT_TEMPERATURE, featherlessDefaultModelId, featherlessModels } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-import { TagMatcher } from "../../utils/tag-matcher"
-import { convertToAiSdkMessages, handleAiSdkError } from "../transform/ai-sdk"
-import { ApiStream } from "../transform/stream"
-import { getModelParams } from "../transform/model-params"
-
-import type { ApiHandlerCreateMessageMetadata } from "../index"
-import { OpenAICompatibleHandler, OpenAICompatibleConfig } from "./openai-compatible"
-
-/**
- * Merge consecutive Anthropic messages that share the same role.
- * DeepSeek R1 does not support successive messages with the same role,
- * so this is needed when the system prompt is injected as a user message
- * before the existing conversation (which may also start with a user message).
- */
-function mergeConsecutiveSameRoleMessages(
-	messages: Anthropic.Messages.MessageParam[],
-): Anthropic.Messages.MessageParam[] {
-	if (messages.length <= 1) {
-		return messages
-	}
-
-	const merged: Anthropic.Messages.MessageParam[] = []
-
-	for (const msg of messages) {
-		const prev = merged[merged.length - 1]
-
-		if (prev && prev.role === msg.role) {
-			const prevBlocks: Anthropic.Messages.ContentBlockParam[] =
-				typeof prev.content === "string" ? [{ type: "text", text: prev.content }] : prev.content
-			const currBlocks: Anthropic.Messages.ContentBlockParam[] =
-				typeof msg.content === "string" ? [{ type: "text", text: msg.content }] : msg.content
-
-			merged[merged.length - 1] = {
-				role: prev.role,
-				content: [...prevBlocks, ...currBlocks],
-			}
-		} else {
-			merged.push(msg)
-		}
-	}
-
-	return merged
-}
-
-export class FeatherlessHandler extends OpenAICompatibleHandler {
-	constructor(options: ApiHandlerOptions) {
-		const modelId = options.apiModelId ?? featherlessDefaultModelId
-		const modelInfo =
-			featherlessModels[modelId as keyof typeof featherlessModels] || featherlessModels[featherlessDefaultModelId]
-
-		const config: OpenAICompatibleConfig = {
-			providerName: "Featherless",
-			baseURL: "https://api.featherless.ai/v1",
-			apiKey: options.featherlessApiKey ?? "not-provided",
-			modelId,
-			modelInfo,
-			modelMaxTokens: options.modelMaxTokens ?? undefined,
-			temperature: options.modelTemperature ?? undefined,
-		}
-
-		super(options, config)
-	}
-
-	override getModel() {
-		const id = this.options.apiModelId ?? featherlessDefaultModelId
-		const info =
-			featherlessModels[id as keyof typeof featherlessModels] || featherlessModels[featherlessDefaultModelId]
-		const isDeepSeekR1 = id.includes("DeepSeek-R1")
-		const defaultTemp = isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0.5
-		const params = getModelParams({
-			format: "openai",
-			modelId: id,
-			model: info,
-			settings: this.options,
-			defaultTemperature: defaultTemp,
-		})
-		return { id, info, ...params }
-	}
-
-	override async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		const model = this.getModel()
-
-		if (model.id.includes("DeepSeek-R1")) {
-			// R1 path: merge system prompt into user messages, use TagMatcher for <think> tags.
-			// mergeConsecutiveSameRoleMessages ensures no two successive messages share the
-			// same role (e.g. the injected system-as-user + original first user message).
-			const r1Messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: systemPrompt }, ...messages]
-			const aiSdkMessages = convertToAiSdkMessages(mergeConsecutiveSameRoleMessages(r1Messages))
-
-			const result = streamText({
-				model: this.getLanguageModel(),
-				messages: aiSdkMessages,
-				temperature: model.temperature ?? 0,
-				maxOutputTokens: this.getMaxOutputTokens(),
-			})
-
-			const matcher = new TagMatcher(
-				"think",
-				(chunk) =>
-					({
-						type: chunk.matched ? "reasoning" : "text",
-						text: chunk.data,
-					}) as const,
-			)
-
-			try {
-				for await (const part of result.fullStream) {
-					if (part.type === "text-delta") {
-						for (const processedChunk of matcher.update(part.text)) {
-							yield processedChunk
-						}
-					}
-				}
-
-				for (const processedChunk of matcher.final()) {
-					yield processedChunk
-				}
-
-				const usage = await result.usage
-				if (usage) {
-					yield this.processUsageMetrics(usage)
-				}
-			} catch (error) {
-				throw handleAiSdkError(error, "Featherless")
-			}
-		} else {
-			yield* super.createMessage(systemPrompt, messages, metadata)
-		}
-	}
-}

+ 0 - 342
src/api/providers/fetchers/__tests__/chutes.spec.ts

@@ -1,342 +0,0 @@
-// Mocks must come first, before imports
-vi.mock("axios")
-
-import type { Mock } from "vitest"
-import type { ModelInfo } from "@roo-code/types"
-import axios from "axios"
-import { getChutesModels } from "../chutes"
-import { chutesModels } from "@roo-code/types"
-
-const mockedAxios = axios as typeof axios & {
-	get: Mock
-}
-
-describe("getChutesModels", () => {
-	beforeEach(() => {
-		vi.clearAllMocks()
-	})
-
-	it("should fetch and parse models successfully", async () => {
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: "test/new-model",
-						object: "model",
-						owned_by: "test",
-						created: 1234567890,
-						context_length: 128000,
-						max_model_len: 8192,
-						input_modalities: ["text"],
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		expect(mockedAxios.get).toHaveBeenCalledWith(
-			"https://llm.chutes.ai/v1/models",
-			expect.objectContaining({
-				headers: expect.objectContaining({
-					Authorization: "Bearer test-api-key",
-				}),
-			}),
-		)
-
-		expect(models["test/new-model"]).toEqual({
-			maxTokens: 8192,
-			contextWindow: 128000,
-			supportsImages: false,
-			supportsPromptCache: false,
-			inputPrice: 0,
-			outputPrice: 0,
-			description: "Chutes AI model: test/new-model",
-		})
-	})
-
-	it("should override hardcoded models with dynamic API data", async () => {
-		// Find any hardcoded model
-		const [modelId] = Object.entries(chutesModels)[0]
-
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: modelId,
-						object: "model",
-						owned_by: "test",
-						created: 1234567890,
-						context_length: 200000, // Different from hardcoded
-						max_model_len: 10000, // Different from hardcoded
-						input_modalities: ["text", "image"],
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		// Dynamic values should override hardcoded
-		expect(models[modelId]).toBeDefined()
-		expect(models[modelId].contextWindow).toBe(200000)
-		expect(models[modelId].maxTokens).toBe(10000)
-		expect(models[modelId].supportsImages).toBe(true)
-	})
-
-	it("should return hardcoded models when API returns empty", async () => {
-		const mockResponse = {
-			data: {
-				data: [],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		// Should still have hardcoded models
-		expect(Object.keys(models).length).toBeGreaterThan(0)
-		expect(models).toEqual(expect.objectContaining(chutesModels))
-	})
-
-	it("should return hardcoded models on API error", async () => {
-		mockedAxios.get.mockRejectedValue(new Error("Network error"))
-
-		const models = await getChutesModels("test-api-key")
-
-		// Should still have hardcoded models
-		expect(Object.keys(models).length).toBeGreaterThan(0)
-		expect(models).toEqual(chutesModels)
-	})
-
-	it("should work without API key", async () => {
-		const mockResponse = {
-			data: {
-				data: [],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels()
-
-		expect(mockedAxios.get).toHaveBeenCalledWith(
-			"https://llm.chutes.ai/v1/models",
-			expect.objectContaining({
-				headers: expect.not.objectContaining({
-					Authorization: expect.anything(),
-				}),
-			}),
-		)
-
-		expect(Object.keys(models).length).toBeGreaterThan(0)
-	})
-
-	it("should detect image support from input_modalities", async () => {
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: "test/image-model",
-						object: "model",
-						owned_by: "test",
-						created: 1234567890,
-						context_length: 128000,
-						max_model_len: 8192,
-						input_modalities: ["text", "image"],
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		expect(models["test/image-model"].supportsImages).toBe(true)
-	})
-
-	it("should accept supported_features containing tools", async () => {
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: "test/tools-model",
-						object: "model",
-						owned_by: "test",
-						created: 1234567890,
-						context_length: 128000,
-						max_model_len: 8192,
-						input_modalities: ["text"],
-						supported_features: ["json_mode", "tools", "reasoning"],
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		expect(models["test/tools-model"]).toBeDefined()
-		expect(models["test/tools-model"].contextWindow).toBe(128000)
-	})
-
-	it("should accept supported_features without tools", async () => {
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: "test/no-tools-model",
-						object: "model",
-						owned_by: "test",
-						created: 1234567890,
-						context_length: 128000,
-						max_model_len: 8192,
-						input_modalities: ["text"],
-						supported_features: ["json_mode", "reasoning"],
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		expect(models["test/no-tools-model"]).toBeDefined()
-		expect(models["test/no-tools-model"].contextWindow).toBe(128000)
-	})
-
-	it("should skip empty objects in API response and still process valid models", async () => {
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: "test/valid-model",
-						object: "model",
-						owned_by: "test",
-						created: 1234567890,
-						context_length: 128000,
-						max_model_len: 8192,
-						input_modalities: ["text"],
-					},
-					{}, // Empty object - should be skipped
-					{
-						id: "test/another-valid-model",
-						object: "model",
-						context_length: 64000,
-						max_model_len: 4096,
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		// Valid models should be processed
-		expect(models["test/valid-model"]).toBeDefined()
-		expect(models["test/valid-model"].contextWindow).toBe(128000)
-		expect(models["test/another-valid-model"]).toBeDefined()
-		expect(models["test/another-valid-model"].contextWindow).toBe(64000)
-	})
-
-	it("should skip models without id field", async () => {
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						// Missing id field
-						object: "model",
-						context_length: 128000,
-						max_model_len: 8192,
-					},
-					{
-						id: "test/valid-model",
-						context_length: 64000,
-						max_model_len: 4096,
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		// Only the valid model should be added
-		expect(models["test/valid-model"]).toBeDefined()
-		// Hardcoded models should still exist
-		expect(Object.keys(models).length).toBeGreaterThan(1)
-	})
-
-	it("should calculate maxTokens fallback when max_model_len is missing", async () => {
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: "test/no-max-len-model",
-						object: "model",
-						context_length: 100000,
-						// max_model_len is missing
-						input_modalities: ["text"],
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		// Should calculate maxTokens as 20% of contextWindow
-		expect(models["test/no-max-len-model"]).toBeDefined()
-		expect(models["test/no-max-len-model"].maxTokens).toBe(20000) // 100000 * 0.2
-		expect(models["test/no-max-len-model"].contextWindow).toBe(100000)
-	})
-
-	it("should gracefully handle response with mixed valid and invalid items", async () => {
-		const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
-
-		const mockResponse = {
-			data: {
-				data: [
-					{
-						id: "test/valid-1",
-						context_length: 128000,
-						max_model_len: 8192,
-					},
-					{}, // Empty - will be skipped
-					null, // Null - will be skipped
-					{
-						id: "", // Empty string id - will be skipped
-						context_length: 64000,
-					},
-					{
-						id: "test/valid-2",
-						context_length: 256000,
-						max_model_len: 16384,
-						supported_features: ["tools"],
-					},
-				],
-			},
-		}
-
-		mockedAxios.get.mockResolvedValue(mockResponse)
-
-		const models = await getChutesModels("test-api-key")
-
-		// Both valid models should be processed
-		expect(models["test/valid-1"]).toBeDefined()
-		expect(models["test/valid-2"]).toBeDefined()
-
-		consoleErrorSpy.mockRestore()
-	})
-})

+ 0 - 42
src/api/providers/fetchers/__tests__/modelCache.spec.ts

@@ -41,8 +41,6 @@ vi.mock("fs", () => ({
 vi.mock("../litellm")
 vi.mock("../openrouter")
 vi.mock("../requesty")
-vi.mock("../unbound")
-vi.mock("../io-intelligence")
 
 // Mock ContextProxy with a simple static instance
 vi.mock("../../../core/config/ContextProxy", () => ({
@@ -63,18 +61,12 @@ import { getModels, getModelsFromCache } from "../modelCache"
 import { getLiteLLMModels } from "../litellm"
 import { getOpenRouterModels } from "../openrouter"
 import { getRequestyModels } from "../requesty"
-import { getUnboundModels } from "../unbound"
-import { getIOIntelligenceModels } from "../io-intelligence"
 
 const mockGetLiteLLMModels = getLiteLLMModels as Mock<typeof getLiteLLMModels>
 const mockGetOpenRouterModels = getOpenRouterModels as Mock<typeof getOpenRouterModels>
 const mockGetRequestyModels = getRequestyModels as Mock<typeof getRequestyModels>
-const mockGetUnboundModels = getUnboundModels as Mock<typeof getUnboundModels>
-const mockGetIOIntelligenceModels = getIOIntelligenceModels as Mock<typeof getIOIntelligenceModels>
 
 const DUMMY_REQUESTY_KEY = "requesty-key-for-testing"
-const DUMMY_UNBOUND_KEY = "unbound-key-for-testing"
-const DUMMY_IOINTELLIGENCE_KEY = "io-intelligence-key-for-testing"
 
 describe("getModels with new GetModelsOptions", () => {
 	beforeEach(() => {
@@ -136,40 +128,6 @@ describe("getModels with new GetModelsOptions", () => {
 		expect(result).toEqual(mockModels)
 	})
 
-	it("calls getUnboundModels with optional API key", async () => {
-		const mockModels = {
-			"unbound/model": {
-				maxTokens: 4096,
-				contextWindow: 8192,
-				supportsPromptCache: false,
-				description: "Unbound model",
-			},
-		}
-		mockGetUnboundModels.mockResolvedValue(mockModels)
-
-		const result = await getModels({ provider: "unbound", apiKey: DUMMY_UNBOUND_KEY })
-
-		expect(mockGetUnboundModels).toHaveBeenCalledWith(DUMMY_UNBOUND_KEY)
-		expect(result).toEqual(mockModels)
-	})
-
-	it("calls IOIntelligenceModels for IO-Intelligence provider", async () => {
-		const mockModels = {
-			"io-intelligence/model": {
-				maxTokens: 4096,
-				contextWindow: 8192,
-				supportsPromptCache: false,
-				description: "IO Intelligence Model",
-			},
-		}
-		mockGetIOIntelligenceModels.mockResolvedValue(mockModels)
-
-		const result = await getModels({ provider: "io-intelligence", apiKey: DUMMY_IOINTELLIGENCE_KEY })
-
-		expect(mockGetIOIntelligenceModels).toHaveBeenCalled()
-		expect(result).toEqual(mockModels)
-	})
-
 	it("handles errors and re-throws them", async () => {
 		const expectedError = new Error("LiteLLM connection failed")
 		mockGetLiteLLMModels.mockRejectedValue(expectedError)

+ 0 - 89
src/api/providers/fetchers/chutes.ts

@@ -1,89 +0,0 @@
-import axios from "axios"
-import { z } from "zod"
-
-import { type ModelInfo, chutesModels } from "@roo-code/types"
-
-import { DEFAULT_HEADERS } from "../constants"
-
-// Chutes models endpoint follows OpenAI /models shape with additional fields.
-// All fields are optional to allow graceful handling of incomplete API responses.
-const ChutesModelSchema = z.object({
-	id: z.string().optional(),
-	object: z.literal("model").optional(),
-	owned_by: z.string().optional(),
-	created: z.number().optional(),
-	context_length: z.number().optional(),
-	max_model_len: z.number().optional(),
-	input_modalities: z.array(z.string()).optional(),
-	supported_features: z.array(z.string()).optional(),
-})
-
-const ChutesModelsResponseSchema = z.object({ data: z.array(ChutesModelSchema) })
-
-type ChutesModelsResponse = z.infer<typeof ChutesModelsResponseSchema>
-
-export async function getChutesModels(apiKey?: string): Promise<Record<string, ModelInfo>> {
-	const headers: Record<string, string> = { ...DEFAULT_HEADERS }
-
-	if (apiKey) {
-		headers["Authorization"] = `Bearer ${apiKey}`
-	}
-
-	const url = "https://llm.chutes.ai/v1/models"
-
-	// Start with hardcoded models as the base.
-	const models: Record<string, ModelInfo> = { ...chutesModels }
-
-	try {
-		const response = await axios.get<ChutesModelsResponse>(url, { headers })
-		const result = ChutesModelsResponseSchema.safeParse(response.data)
-
-		// Graceful fallback: use parsed data if valid, otherwise fall back to raw response data.
-		// This mirrors the OpenRouter pattern for handling API responses with some invalid items.
-		const data = result.success ? result.data.data : response.data?.data
-
-		if (!result.success) {
-			console.error(`Error parsing Chutes models response: ${JSON.stringify(result.error.format(), null, 2)}`)
-		}
-
-		if (!data || !Array.isArray(data)) {
-			console.error("Chutes models response missing data array")
-			return models
-		}
-
-		for (const m of data) {
-			// Skip items missing required fields (e.g., empty objects from API)
-			if (!m || typeof m.id !== "string" || !m.id) {
-				continue
-			}
-
-			const contextWindow =
-				typeof m.context_length === "number" && Number.isFinite(m.context_length) ? m.context_length : undefined
-			const maxModelLen =
-				typeof m.max_model_len === "number" && Number.isFinite(m.max_model_len) ? m.max_model_len : undefined
-
-			// Skip models without valid context window information
-			if (!contextWindow) {
-				continue
-			}
-
-			const info: ModelInfo = {
-				maxTokens: maxModelLen ?? Math.ceil(contextWindow * 0.2),
-				contextWindow,
-				supportsImages: (m.input_modalities || []).includes("image"),
-				supportsPromptCache: false,
-				inputPrice: 0,
-				outputPrice: 0,
-				description: `Chutes AI model: ${m.id}`,
-			}
-
-			// Union: dynamic models override hardcoded ones if they have the same ID.
-			models[m.id] = info
-		}
-	} catch (error) {
-		console.error(`Error fetching Chutes models: ${error instanceof Error ? error.message : String(error)}`)
-		// On error, still return hardcoded models.
-	}
-
-	return models
-}

+ 0 - 71
src/api/providers/fetchers/deepinfra.ts

@@ -1,71 +0,0 @@
-import axios from "axios"
-import { z } from "zod"
-
-import { type ModelInfo } from "@roo-code/types"
-
-import { DEFAULT_HEADERS } from "../constants"
-
-// DeepInfra models endpoint follows OpenAI /models shape with an added metadata object.
-
-const DeepInfraModelSchema = z.object({
-	id: z.string(),
-	object: z.literal("model").optional(),
-	owned_by: z.string().optional(),
-	created: z.number().optional(),
-	root: z.string().optional(),
-	metadata: z
-		.object({
-			description: z.string().optional(),
-			context_length: z.number().optional(),
-			max_tokens: z.number().optional(),
-			tags: z.array(z.string()).optional(), // e.g., ["vision", "prompt_cache"]
-			pricing: z
-				.object({
-					input_tokens: z.number().optional(),
-					output_tokens: z.number().optional(),
-					cache_read_tokens: z.number().optional(),
-				})
-				.optional(),
-		})
-		.optional(),
-})
-
-const DeepInfraModelsResponseSchema = z.object({ data: z.array(DeepInfraModelSchema) })
-
-export async function getDeepInfraModels(
-	apiKey?: string,
-	baseUrl: string = "https://api.deepinfra.com/v1/openai",
-): Promise<Record<string, ModelInfo>> {
-	const headers: Record<string, string> = { ...DEFAULT_HEADERS }
-	if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
-
-	const url = `${baseUrl.replace(/\/$/, "")}/models`
-	const models: Record<string, ModelInfo> = {}
-
-	const response = await axios.get(url, { headers })
-	const parsed = DeepInfraModelsResponseSchema.safeParse(response.data)
-	const data = parsed.success ? parsed.data.data : response.data?.data || []
-
-	for (const m of data as Array<z.infer<typeof DeepInfraModelSchema>>) {
-		const meta = m.metadata || {}
-		const tags = meta.tags || []
-
-		const contextWindow = typeof meta.context_length === "number" ? meta.context_length : 8192
-		const maxTokens = typeof meta.max_tokens === "number" ? meta.max_tokens : Math.ceil(contextWindow * 0.2)
-
-		const info: ModelInfo = {
-			maxTokens,
-			contextWindow,
-			supportsImages: tags.includes("vision"),
-			supportsPromptCache: tags.includes("prompt_cache"),
-			inputPrice: meta.pricing?.input_tokens,
-			outputPrice: meta.pricing?.output_tokens,
-			cacheReadsPrice: meta.pricing?.cache_read_tokens,
-			description: meta.description,
-		}
-
-		models[m.id] = info
-	}
-
-	return models
-}

+ 0 - 252
src/api/providers/fetchers/huggingface.ts

@@ -1,252 +0,0 @@
-import axios from "axios"
-import { z } from "zod"
-
-import {
-	type ModelInfo,
-	type ModelRecord,
-	HUGGINGFACE_API_URL,
-	HUGGINGFACE_CACHE_DURATION,
-	HUGGINGFACE_DEFAULT_MAX_TOKENS,
-	HUGGINGFACE_DEFAULT_CONTEXT_WINDOW,
-} from "@roo-code/types"
-
-const huggingFaceProviderSchema = z.object({
-	provider: z.string(),
-	status: z.enum(["live", "staging", "error"]),
-	supports_tools: z.boolean().optional(),
-	supports_structured_output: z.boolean().optional(),
-	context_length: z.number().optional(),
-	pricing: z
-		.object({
-			input: z.number(),
-			output: z.number(),
-		})
-		.optional(),
-})
-
-/**
- * Represents a provider that can serve a HuggingFace model.
- *
- * @property provider - The provider identifier (e.g., "sambanova", "together")
- * @property status - The current status of the provider
- * @property supports_tools - Whether the provider supports tool/function calling
- * @property supports_structured_output - Whether the provider supports structured output
- * @property context_length - The maximum context length supported by this provider
- * @property pricing - The pricing information for input/output tokens
- */
-export type HuggingFaceProvider = z.infer<typeof huggingFaceProviderSchema>
-
-const huggingFaceModelSchema = z.object({
-	id: z.string(),
-	object: z.literal("model"),
-	created: z.number(),
-	owned_by: z.string(),
-	providers: z.array(huggingFaceProviderSchema),
-})
-
-/**
- * Represents a HuggingFace model available through the router API
- *
- * @property id - The unique identifier of the model
- * @property object - The object type (always "model")
- * @property created - Unix timestamp of when the model was created
- * @property owned_by - The organization that owns the model
- * @property providers - List of providers that can serve this model
- */
-export type HuggingFaceModel = z.infer<typeof huggingFaceModelSchema>
-
-const huggingFaceApiResponseSchema = z.object({
-	object: z.string(),
-	data: z.array(huggingFaceModelSchema),
-})
-
-type HuggingFaceApiResponse = z.infer<typeof huggingFaceApiResponseSchema>
-
-interface CacheEntry {
-	data: ModelRecord
-	rawModels?: HuggingFaceModel[]
-	timestamp: number
-}
-
-let cache: CacheEntry | null = null
-
-/**
- * Parse a HuggingFace model into ModelInfo format.
- *
- * @param model - The HuggingFace model to parse
- * @param provider - Optional specific provider to use for capabilities
- * @returns ModelInfo object compatible with the application's model system
- */
-function parseHuggingFaceModel(model: HuggingFaceModel, provider?: HuggingFaceProvider): ModelInfo {
-	// Use provider-specific values if available, otherwise find first provider with values.
-	const contextLength =
-		provider?.context_length ||
-		model.providers.find((p) => p.context_length)?.context_length ||
-		HUGGINGFACE_DEFAULT_CONTEXT_WINDOW
-
-	const pricing = provider?.pricing || model.providers.find((p) => p.pricing)?.pricing
-
-	// Include provider name in description if specific provider is given.
-	const description = provider ? `${model.id} via ${provider.provider}` : `${model.id} via HuggingFace`
-
-	return {
-		maxTokens: Math.min(contextLength, HUGGINGFACE_DEFAULT_MAX_TOKENS),
-		contextWindow: contextLength,
-		supportsImages: false, // HuggingFace API doesn't provide this info yet.
-		supportsPromptCache: false,
-		inputPrice: pricing?.input,
-		outputPrice: pricing?.output,
-		description,
-	}
-}
-
-/**
- * Fetches available models from HuggingFace
- *
- * @returns A promise that resolves to a record of model IDs to model info
- * @throws Will throw an error if the request fails
- */
-export async function getHuggingFaceModels(): Promise<ModelRecord> {
-	const now = Date.now()
-
-	if (cache && now - cache.timestamp < HUGGINGFACE_CACHE_DURATION) {
-		return cache.data
-	}
-
-	const models: ModelRecord = {}
-
-	try {
-		const response = await axios.get<HuggingFaceApiResponse>(HUGGINGFACE_API_URL, {
-			headers: {
-				"Upgrade-Insecure-Requests": "1",
-				"Sec-Fetch-Dest": "document",
-				"Sec-Fetch-Mode": "navigate",
-				"Sec-Fetch-Site": "none",
-				"Sec-Fetch-User": "?1",
-				Priority: "u=0, i",
-				Pragma: "no-cache",
-				"Cache-Control": "no-cache",
-			},
-			timeout: 10000,
-		})
-
-		const result = huggingFaceApiResponseSchema.safeParse(response.data)
-
-		if (!result.success) {
-			console.error("HuggingFace models response validation failed:", result.error.format())
-			throw new Error("Invalid response format from HuggingFace API")
-		}
-
-		const validModels = result.data.data.filter((model) => model.providers.length > 0)
-
-		for (const model of validModels) {
-			// Add the base model.
-			models[model.id] = parseHuggingFaceModel(model)
-
-			// Add provider-specific variants for all live providers.
-			for (const provider of model.providers) {
-				if (provider.status === "live") {
-					const providerKey = `${model.id}:${provider.provider}`
-					const providerModel = parseHuggingFaceModel(model, provider)
-
-					// Always add provider variants to show all available providers.
-					models[providerKey] = providerModel
-				}
-			}
-		}
-
-		cache = { data: models, rawModels: validModels, timestamp: now }
-
-		return models
-	} catch (error) {
-		console.error("Error fetching HuggingFace models:", error)
-
-		if (cache) {
-			return cache.data
-		}
-
-		if (axios.isAxiosError(error)) {
-			if (error.response) {
-				throw new Error(
-					`Failed to fetch HuggingFace models: ${error.response.status} ${error.response.statusText}`,
-				)
-			} else if (error.request) {
-				throw new Error(
-					"Failed to fetch HuggingFace models: No response from server. Check your internet connection.",
-				)
-			}
-		}
-
-		throw new Error(
-			`Failed to fetch HuggingFace models: ${error instanceof Error ? error.message : "Unknown error"}`,
-		)
-	}
-}
-
-/**
- * Get cached models without making an API request.
- */
-export function getCachedHuggingFaceModels(): ModelRecord | null {
-	return cache?.data || null
-}
-
-/**
- * Get cached raw models for UI display.
- */
-export function getCachedRawHuggingFaceModels(): HuggingFaceModel[] | null {
-	return cache?.rawModels || null
-}
-
-export function clearHuggingFaceCache(): void {
-	cache = null
-}
-
-export interface HuggingFaceModelsResponse {
-	models: HuggingFaceModel[]
-	cached: boolean
-	timestamp: number
-}
-
-export async function getHuggingFaceModelsWithMetadata(): Promise<HuggingFaceModelsResponse> {
-	try {
-		// First, trigger the fetch to populate cache.
-		await getHuggingFaceModels()
-
-		// Get the raw models from cache.
-		const cachedRawModels = getCachedRawHuggingFaceModels()
-
-		if (cachedRawModels) {
-			return {
-				models: cachedRawModels,
-				cached: true,
-				timestamp: Date.now(),
-			}
-		}
-
-		// If no cached raw models, fetch directly from API.
-		const response = await axios.get(HUGGINGFACE_API_URL, {
-			headers: {
-				"Upgrade-Insecure-Requests": "1",
-				"Sec-Fetch-Dest": "document",
-				"Sec-Fetch-Mode": "navigate",
-				"Sec-Fetch-Site": "none",
-				"Sec-Fetch-User": "?1",
-				Priority: "u=0, i",
-				Pragma: "no-cache",
-				"Cache-Control": "no-cache",
-			},
-			timeout: 10000,
-		})
-
-		const models = response.data?.data || []
-
-		return {
-			models,
-			cached: false,
-			timestamp: Date.now(),
-		}
-	} catch (error) {
-		console.error("Failed to get HuggingFace models:", error)
-		return { models: [], cached: false, timestamp: Date.now() }
-	}
-}

+ 0 - 158
src/api/providers/fetchers/io-intelligence.ts

@@ -1,158 +0,0 @@
-import axios from "axios"
-import { z } from "zod"
-
-import { type ModelInfo, type ModelRecord, IO_INTELLIGENCE_CACHE_DURATION } from "@roo-code/types"
-
-const ioIntelligenceModelSchema = z.object({
-	id: z.string(),
-	object: z.literal("model"),
-	created: z.number(),
-	owned_by: z.string(),
-	root: z.string().nullable().optional(),
-	parent: z.string().nullable().optional(),
-	max_model_len: z.number().nullable().optional(),
-	permission: z.array(
-		z.object({
-			id: z.string(),
-			object: z.literal("model_permission"),
-			created: z.number(),
-			allow_create_engine: z.boolean(),
-			allow_sampling: z.boolean(),
-			allow_logprobs: z.boolean(),
-			allow_search_indices: z.boolean(),
-			allow_view: z.boolean(),
-			allow_fine_tuning: z.boolean(),
-			organization: z.string(),
-			group: z.string().nullable(),
-			is_blocking: z.boolean(),
-		}),
-	),
-})
-
-export type IOIntelligenceModel = z.infer<typeof ioIntelligenceModelSchema>
-
-const ioIntelligenceApiResponseSchema = z.object({
-	object: z.literal("list"),
-	data: z.array(ioIntelligenceModelSchema),
-})
-
-type IOIntelligenceApiResponse = z.infer<typeof ioIntelligenceApiResponseSchema>
-
-interface CacheEntry {
-	data: ModelRecord
-	timestamp: number
-}
-
-let cache: CacheEntry | null = null
-
-/**
- * Model context length mapping based on the documentation
- * <mcreference link="https://docs.io.net/reference/get-started-with-io-intelligence-api" index="1">1</mcreference>
- */
-const MODEL_CONTEXT_LENGTHS: Record<string, number> = {
-	"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": 430000,
-	"deepseek-ai/DeepSeek-R1-0528": 128000,
-	"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": 106000,
-	"openai/gpt-oss-120b": 131072,
-}
-
-const VISION_MODELS = new Set([
-	"Qwen/Qwen2.5-VL-32B-Instruct",
-	"meta-llama/Llama-3.2-90B-Vision-Instruct",
-	"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
-])
-
-function parseIOIntelligenceModel(model: IOIntelligenceModel): ModelInfo {
-	const contextLength = MODEL_CONTEXT_LENGTHS[model.id] || 8192
-	// Cap maxTokens at 32k for very large context windows, or 20% of context length, whichever is smaller.
-	const maxTokens = Math.min(contextLength, Math.ceil(contextLength * 0.2), 32768)
-	const supportsImages = VISION_MODELS.has(model.id)
-
-	return {
-		maxTokens,
-		contextWindow: contextLength,
-		supportsImages,
-		supportsPromptCache: false,
-		description: `${model.id} via IO Intelligence`,
-	}
-}
-
-/**
- * Fetches available models from IO Intelligence
- * <mcreference link="https://docs.io.net/reference/get-started-with-io-intelligence-api" index="1">1</mcreference>
- */
-export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRecord> {
-	const now = Date.now()
-
-	if (cache && now - cache.timestamp < IO_INTELLIGENCE_CACHE_DURATION) {
-		return cache.data
-	}
-
-	const models: ModelRecord = {}
-
-	try {
-		const headers: Record<string, string> = {
-			"Content-Type": "application/json",
-		}
-
-		if (apiKey) {
-			headers.Authorization = `Bearer ${apiKey}`
-		} else {
-			console.error("IO Intelligence API key is required")
-			throw new Error("IO Intelligence API key is required")
-		}
-
-		const response = await axios.get<IOIntelligenceApiResponse>(
-			"https://api.intelligence.io.solutions/api/v1/models",
-			{
-				headers,
-				timeout: 10_000,
-			},
-		)
-
-		const result = ioIntelligenceApiResponseSchema.safeParse(response.data)
-
-		if (!result.success) {
-			console.error("IO Intelligence models response validation failed:", result.error.format())
-			throw new Error("Invalid response format from IO Intelligence API")
-		}
-
-		for (const model of result.data.data) {
-			models[model.id] = parseIOIntelligenceModel(model)
-		}
-
-		cache = { data: models, timestamp: now }
-
-		return models
-	} catch (error) {
-		console.error("Error fetching IO Intelligence models:", error)
-
-		if (cache) {
-			return cache.data
-		}
-
-		if (axios.isAxiosError(error)) {
-			if (error.response) {
-				throw new Error(
-					`Failed to fetch IO Intelligence models: ${error.response.status} ${error.response.statusText}`,
-				)
-			} else if (error.request) {
-				throw new Error(
-					"Failed to fetch IO Intelligence models: No response from server. Check your internet connection.",
-				)
-			}
-		}
-
-		throw new Error(
-			`Failed to fetch IO Intelligence models: ${error instanceof Error ? error.message : "Unknown error"}`,
-		)
-	}
-}
-
-export function getCachedIOIntelligenceModels(): ModelRecord | null {
-	return cache?.data || null
-}
-
-export function clearIOIntelligenceCache(): void {
-	cache = null
-}

+ 0 - 22
src/api/providers/fetchers/modelCache.ts

@@ -19,16 +19,11 @@ import { fileExistsAtPath } from "../../../utils/fs"
 import { getOpenRouterModels } from "./openrouter"
 import { getVercelAiGatewayModels } from "./vercel-ai-gateway"
 import { getRequestyModels } from "./requesty"
-import { getUnboundModels } from "./unbound"
 import { getLiteLLMModels } from "./litellm"
 import { GetModelsOptions } from "../../../shared/api"
 import { getOllamaModels } from "./ollama"
 import { getLMStudioModels } from "./lmstudio"
-import { getIOIntelligenceModels } from "./io-intelligence"
-import { getDeepInfraModels } from "./deepinfra"
-import { getHuggingFaceModels } from "./huggingface"
 import { getRooModels } from "./roo"
-import { getChutesModels } from "./chutes"
 
 const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
 
@@ -73,10 +68,6 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise<Model
 			// Requesty models endpoint requires an API key for per-user custom policies.
 			models = await getRequestyModels(options.baseUrl, options.apiKey)
 			break
-		case "unbound":
-			// Unbound models endpoint requires an API key to fetch application specific models.
-			models = await getUnboundModels(options.apiKey)
-			break
 		case "litellm":
 			// Type safety ensures apiKey and baseUrl are always provided for LiteLLM.
 			models = await getLiteLLMModels(options.apiKey, options.baseUrl)
@@ -87,27 +78,15 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise<Model
 		case "lmstudio":
 			models = await getLMStudioModels(options.baseUrl)
 			break
-		case "deepinfra":
-			models = await getDeepInfraModels(options.apiKey, options.baseUrl)
-			break
-		case "io-intelligence":
-			models = await getIOIntelligenceModels(options.apiKey)
-			break
 		case "vercel-ai-gateway":
 			models = await getVercelAiGatewayModels()
 			break
-		case "huggingface":
-			models = await getHuggingFaceModels()
-			break
 		case "roo": {
 			// Roo Code Cloud provider requires baseUrl and optional apiKey
 			const rooBaseUrl = options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
 			models = await getRooModels(rooBaseUrl, options.apiKey)
 			break
 		}
-		case "chutes":
-			models = await getChutesModels(options.apiKey)
-			break
 		default: {
 			// Ensures router is exhaustively checked if RouterName is a strict union.
 			const exhaustiveCheck: never = provider
@@ -249,7 +228,6 @@ export async function initializeModelCacheRefresh(): Promise<void> {
 		const publicProviders: Array<{ provider: RouterName; options: GetModelsOptions }> = [
 			{ provider: "openrouter", options: { provider: "openrouter" } },
 			{ provider: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } },
-			{ provider: "chutes", options: { provider: "chutes" } },
 		]
 
 		// Refresh each provider in background (fire and forget)

+ 0 - 52
src/api/providers/fetchers/unbound.ts

@@ -1,52 +0,0 @@
-import axios from "axios"
-
-import type { ModelInfo } from "@roo-code/types"
-
-export async function getUnboundModels(apiKey?: string | null): Promise<Record<string, ModelInfo>> {
-	const models: Record<string, ModelInfo> = {}
-
-	try {
-		const headers: Record<string, string> = {}
-
-		if (apiKey) {
-			headers["Authorization"] = `Bearer ${apiKey}`
-		}
-
-		const response = await axios.get("https://api.getunbound.ai/models", { headers })
-
-		if (response.data) {
-			const rawModels: Record<string, any> = response.data
-
-			for (const [modelId, model] of Object.entries(rawModels)) {
-				const modelInfo: ModelInfo = {
-					maxTokens: model?.maxTokens ? parseInt(model.maxTokens) : undefined,
-					contextWindow: model?.contextWindow ? parseInt(model.contextWindow) : 0,
-					supportsImages: model?.supportsImages ?? false,
-					supportsPromptCache: model?.supportsPromptCaching ?? false,
-					inputPrice: model?.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined,
-					outputPrice: model?.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined,
-					cacheWritesPrice: model?.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined,
-					cacheReadsPrice: model?.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined,
-				}
-
-				switch (true) {
-					case modelId.startsWith("anthropic/"):
-						// Set max tokens to 8192 for supported Anthropic models
-						if (modelInfo.maxTokens !== 4096) {
-							modelInfo.maxTokens = 8192
-						}
-						break
-					default:
-						break
-				}
-
-				models[modelId] = modelInfo
-			}
-		}
-	} catch (error) {
-		console.error(`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
-		throw new Error(`Failed to fetch Unbound models: ${error instanceof Error ? error.message : "Unknown error"}`)
-	}
-
-	return models
-}

+ 0 - 181
src/api/providers/groq.ts

@@ -1,181 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-import { createGroq } from "@ai-sdk/groq"
-import { streamText, generateText, ToolSet } from "ai"
-
-import { groqModels, groqDefaultModelId, type ModelInfo } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-
-import {
-	convertToAiSdkMessages,
-	convertToolsForAiSdk,
-	processAiSdkStreamPart,
-	mapToolChoice,
-	handleAiSdkError,
-} from "../transform/ai-sdk"
-import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
-import { getModelParams } from "../transform/model-params"
-
-import { DEFAULT_HEADERS } from "./constants"
-import { BaseProvider } from "./base-provider"
-import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
-
-const GROQ_DEFAULT_TEMPERATURE = 0.5
-
-/**
- * Groq provider using the dedicated @ai-sdk/groq package.
- * Provides native support for reasoning models and prompt caching.
- */
-export class GroqHandler extends BaseProvider implements SingleCompletionHandler {
-	protected options: ApiHandlerOptions
-	protected provider: ReturnType<typeof createGroq>
-
-	constructor(options: ApiHandlerOptions) {
-		super()
-		this.options = options
-
-		// Create the Groq provider using AI SDK
-		this.provider = createGroq({
-			baseURL: "https://api.groq.com/openai/v1",
-			apiKey: options.groqApiKey ?? "not-provided",
-			headers: DEFAULT_HEADERS,
-		})
-	}
-
-	override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number } {
-		const id = this.options.apiModelId ?? groqDefaultModelId
-		const info = groqModels[id as keyof typeof groqModels] || groqModels[groqDefaultModelId]
-		const params = getModelParams({
-			format: "openai",
-			modelId: id,
-			model: info,
-			settings: this.options,
-			defaultTemperature: GROQ_DEFAULT_TEMPERATURE,
-		})
-		return { id, info, ...params }
-	}
-
-	/**
-	 * Get the language model for the configured model ID.
-	 */
-	protected getLanguageModel() {
-		const { id } = this.getModel()
-		return this.provider(id)
-	}
-
-	/**
-	 * Process usage metrics from the AI SDK response, including Groq's cache metrics.
-	 * Groq provides cache hit/miss info via providerMetadata for supported models.
-	 */
-	protected processUsageMetrics(
-		usage: {
-			inputTokens?: number
-			outputTokens?: number
-			details?: {
-				cachedInputTokens?: number
-				reasoningTokens?: number
-			}
-		},
-		providerMetadata?: {
-			groq?: {
-				promptCacheHitTokens?: number
-				promptCacheMissTokens?: number
-			}
-		},
-	): ApiStreamUsageChunk {
-		// Extract cache metrics from Groq's providerMetadata
-		const cacheReadTokens = providerMetadata?.groq?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
-		const cacheWriteTokens = providerMetadata?.groq?.promptCacheMissTokens
-
-		return {
-			type: "usage",
-			inputTokens: usage.inputTokens || 0,
-			outputTokens: usage.outputTokens || 0,
-			cacheReadTokens,
-			cacheWriteTokens,
-			reasoningTokens: usage.details?.reasoningTokens,
-		}
-	}
-
-	/**
-	 * Get the max tokens parameter to include in the request.
-	 */
-	protected getMaxOutputTokens(): number | undefined {
-		const { info } = this.getModel()
-		return this.options.modelMaxTokens || info.maxTokens || undefined
-	}
-
-	/**
-	 * Create a message stream using the AI SDK.
-	 * Groq supports reasoning for models like qwen/qwen3-32b via reasoningFormat: 'parsed'.
-	 */
-	override async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		const { temperature } = this.getModel()
-		const languageModel = this.getLanguageModel()
-
-		// Convert messages to AI SDK format
-		const aiSdkMessages = convertToAiSdkMessages(messages)
-
-		// Convert tools to OpenAI format first, then to AI SDK format
-		const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
-		const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
-
-		// Build the request options
-		const requestOptions: Parameters<typeof streamText>[0] = {
-			model: languageModel,
-			system: systemPrompt,
-			messages: aiSdkMessages,
-			temperature: this.options.modelTemperature ?? temperature ?? GROQ_DEFAULT_TEMPERATURE,
-			maxOutputTokens: this.getMaxOutputTokens(),
-			tools: aiSdkTools,
-			toolChoice: mapToolChoice(metadata?.tool_choice),
-		}
-
-		// Use streamText for streaming responses
-		const result = streamText(requestOptions)
-
-		try {
-			// Process the full stream to get all events including reasoning
-			for await (const part of result.fullStream) {
-				for (const chunk of processAiSdkStreamPart(part)) {
-					yield chunk
-				}
-			}
-
-			// Yield usage metrics at the end, including cache metrics from providerMetadata
-			const usage = await result.usage
-			const providerMetadata = await result.providerMetadata
-			if (usage) {
-				yield this.processUsageMetrics(usage, providerMetadata as any)
-			}
-		} catch (error) {
-			// Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.)
-			throw handleAiSdkError(error, "Groq")
-		}
-	}
-
-	/**
-	 * Complete a prompt using the AI SDK generateText.
-	 */
-	async completePrompt(prompt: string): Promise<string> {
-		const { temperature } = this.getModel()
-		const languageModel = this.getLanguageModel()
-
-		const { text } = await generateText({
-			model: languageModel,
-			prompt,
-			maxOutputTokens: this.getMaxOutputTokens(),
-			temperature: this.options.modelTemperature ?? temperature ?? GROQ_DEFAULT_TEMPERATURE,
-		})
-
-		return text
-	}
-
-	override isAiSdkProvider(): boolean {
-		return true
-	}
-}

+ 0 - 215
src/api/providers/huggingface.ts

@@ -1,215 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
-import { streamText, generateText, ToolSet } from "ai"
-
-import type { ModelRecord, ModelInfo } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-
-import {
-	convertToAiSdkMessages,
-	convertToolsForAiSdk,
-	processAiSdkStreamPart,
-	mapToolChoice,
-	handleAiSdkError,
-} from "../transform/ai-sdk"
-import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
-import { getModelParams } from "../transform/model-params"
-
-import { DEFAULT_HEADERS } from "./constants"
-import { BaseProvider } from "./base-provider"
-import { getHuggingFaceModels, getCachedHuggingFaceModels } from "./fetchers/huggingface"
-import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
-
-const HUGGINGFACE_DEFAULT_TEMPERATURE = 0.7
-
-/**
- * HuggingFace provider using @ai-sdk/openai-compatible for OpenAI-compatible API.
- * Uses HuggingFace's OpenAI-compatible endpoint to enable tool message support.
- * @see https://github.com/vercel/ai/issues/10766 - Workaround for tool messages not supported in @ai-sdk/huggingface
- */
-export class HuggingFaceHandler extends BaseProvider implements SingleCompletionHandler {
-	protected options: ApiHandlerOptions
-	protected provider: ReturnType<typeof createOpenAICompatible>
-	private modelCache: ModelRecord | null = null
-
-	constructor(options: ApiHandlerOptions) {
-		super()
-		this.options = options
-
-		if (!this.options.huggingFaceApiKey) {
-			throw new Error("Hugging Face API key is required")
-		}
-
-		// Create an OpenAI-compatible provider pointing to HuggingFace's /v1 endpoint
-		// This fixes "tool messages not supported" error - the HuggingFace SDK doesn't
-		// properly handle function_call_output format, but OpenAI SDK does
-		this.provider = createOpenAICompatible({
-			name: "huggingface",
-			baseURL: "https://router.huggingface.co/v1",
-			apiKey: this.options.huggingFaceApiKey,
-			headers: DEFAULT_HEADERS,
-		})
-
-		// Try to get cached models first
-		this.modelCache = getCachedHuggingFaceModels()
-
-		// Fetch models asynchronously
-		this.fetchModels()
-	}
-
-	private async fetchModels() {
-		try {
-			this.modelCache = await getHuggingFaceModels()
-		} catch (error) {
-			console.error("Failed to fetch HuggingFace models:", error)
-		}
-	}
-
-	override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number } {
-		const id = this.options.huggingFaceModelId || "meta-llama/Llama-3.3-70B-Instruct"
-
-		// Try to get model info from cache
-		const cachedInfo = this.modelCache?.[id]
-
-		const info: ModelInfo = cachedInfo || {
-			maxTokens: 8192,
-			contextWindow: 131072,
-			supportsImages: false,
-			supportsPromptCache: false,
-		}
-
-		const params = getModelParams({
-			format: "openai",
-			modelId: id,
-			model: info,
-			settings: this.options,
-			defaultTemperature: HUGGINGFACE_DEFAULT_TEMPERATURE,
-		})
-
-		return { id, info, ...params }
-	}
-
-	/**
-	 * Get the language model for the configured model ID.
-	 */
-	protected getLanguageModel() {
-		const { id } = this.getModel()
-		return this.provider(id)
-	}
-
-	/**
-	 * Process usage metrics from the AI SDK response.
-	 */
-	protected processUsageMetrics(
-		usage: {
-			inputTokens?: number
-			outputTokens?: number
-			details?: {
-				cachedInputTokens?: number
-				reasoningTokens?: number
-			}
-		},
-		providerMetadata?: {
-			huggingface?: {
-				promptCacheHitTokens?: number
-				promptCacheMissTokens?: number
-			}
-		},
-	): ApiStreamUsageChunk {
-		// Extract cache metrics from HuggingFace's providerMetadata if available
-		const cacheReadTokens = providerMetadata?.huggingface?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
-		const cacheWriteTokens = providerMetadata?.huggingface?.promptCacheMissTokens
-
-		return {
-			type: "usage",
-			inputTokens: usage.inputTokens || 0,
-			outputTokens: usage.outputTokens || 0,
-			cacheReadTokens,
-			cacheWriteTokens,
-			reasoningTokens: usage.details?.reasoningTokens,
-		}
-	}
-
-	/**
-	 * Get the max tokens parameter to include in the request.
-	 */
-	protected getMaxOutputTokens(): number | undefined {
-		const { info } = this.getModel()
-		return this.options.modelMaxTokens || info.maxTokens || undefined
-	}
-
-	/**
-	 * Create a message stream using the AI SDK.
-	 */
-	override async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		const { temperature } = this.getModel()
-		const languageModel = this.getLanguageModel()
-
-		// Convert messages to AI SDK format
-		const aiSdkMessages = convertToAiSdkMessages(messages)
-
-		// Convert tools to OpenAI format first, then to AI SDK format
-		const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
-		const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
-
-		// Build the request options
-		const requestOptions: Parameters<typeof streamText>[0] = {
-			model: languageModel,
-			system: systemPrompt,
-			messages: aiSdkMessages,
-			temperature: this.options.modelTemperature ?? temperature ?? HUGGINGFACE_DEFAULT_TEMPERATURE,
-			maxOutputTokens: this.getMaxOutputTokens(),
-			tools: aiSdkTools,
-			toolChoice: mapToolChoice(metadata?.tool_choice),
-		}
-
-		// Use streamText for streaming responses
-		const result = streamText(requestOptions)
-
-		try {
-			// Process the full stream to get all events
-			for await (const part of result.fullStream) {
-				// Use the processAiSdkStreamPart utility to convert stream parts
-				for (const chunk of processAiSdkStreamPart(part)) {
-					yield chunk
-				}
-			}
-
-			// Yield usage metrics at the end, including cache metrics from providerMetadata
-			const usage = await result.usage
-			const providerMetadata = await result.providerMetadata
-			if (usage) {
-				yield this.processUsageMetrics(usage, providerMetadata as any)
-			}
-		} catch (error) {
-			// Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.)
-			throw handleAiSdkError(error, "HuggingFace")
-		}
-	}
-
-	/**
-	 * Complete a prompt using the AI SDK generateText.
-	 */
-	async completePrompt(prompt: string): Promise<string> {
-		const { temperature } = this.getModel()
-		const languageModel = this.getLanguageModel()
-
-		const { text } = await generateText({
-			model: languageModel,
-			prompt,
-			maxOutputTokens: this.getMaxOutputTokens(),
-			temperature: this.options.modelTemperature ?? temperature ?? HUGGINGFACE_DEFAULT_TEMPERATURE,
-		})
-
-		return text
-	}
-
-	override isAiSdkProvider(): boolean {
-		return true
-	}
-}

+ 0 - 9
src/api/providers/index.ts

@@ -1,16 +1,10 @@
 export { AnthropicVertexHandler } from "./anthropic-vertex"
 export { AnthropicHandler } from "./anthropic"
 export { AwsBedrockHandler } from "./bedrock"
-export { CerebrasHandler } from "./cerebras"
-export { ChutesHandler } from "./chutes"
 export { DeepSeekHandler } from "./deepseek"
-export { DoubaoHandler } from "./doubao"
 export { MoonshotHandler } from "./moonshot"
 export { FakeAIHandler } from "./fake-ai"
 export { GeminiHandler } from "./gemini"
-export { GroqHandler } from "./groq"
-export { HuggingFaceHandler } from "./huggingface"
-export { IOIntelligenceHandler } from "./io-intelligence"
 export { LiteLLMHandler } from "./lite-llm"
 export { LmStudioHandler } from "./lm-studio"
 export { MistralHandler } from "./mistral"
@@ -23,15 +17,12 @@ export { OpenRouterHandler } from "./openrouter"
 export { QwenCodeHandler } from "./qwen-code"
 export { RequestyHandler } from "./requesty"
 export { SambaNovaHandler } from "./sambanova"
-export { UnboundHandler } from "./unbound"
 export { VertexHandler } from "./vertex"
 export { VsCodeLmHandler } from "./vscode-lm"
 export { XAIHandler } from "./xai"
 export { ZAiHandler } from "./zai"
 export { FireworksHandler } from "./fireworks"
 export { RooHandler } from "./roo"
-export { FeatherlessHandler } from "./featherless"
 export { VercelAiGatewayHandler } from "./vercel-ai-gateway"
-export { DeepInfraHandler } from "./deepinfra"
 export { MiniMaxHandler } from "./minimax"
 export { BasetenHandler } from "./baseten"

+ 0 - 62
src/api/providers/io-intelligence.ts

@@ -1,62 +0,0 @@
-import {
-	ioIntelligenceDefaultModelId,
-	ioIntelligenceModels,
-	type IOIntelligenceModelId,
-	type ModelInfo,
-} from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-
-import { getModelParams } from "../transform/model-params"
-
-import { OpenAICompatibleHandler, type OpenAICompatibleConfig } from "./openai-compatible"
-
-export class IOIntelligenceHandler extends OpenAICompatibleHandler {
-	constructor(options: ApiHandlerOptions) {
-		if (!options.ioIntelligenceApiKey) {
-			throw new Error("IO Intelligence API key is required")
-		}
-
-		const modelId = options.ioIntelligenceModelId ?? ioIntelligenceDefaultModelId
-		const modelInfo: ModelInfo = ioIntelligenceModels[modelId as IOIntelligenceModelId] ??
-			ioIntelligenceModels[ioIntelligenceDefaultModelId] ?? {
-				maxTokens: 8192,
-				contextWindow: 128000,
-				supportsImages: false,
-				supportsPromptCache: false,
-			}
-
-		const config: OpenAICompatibleConfig = {
-			providerName: "IO Intelligence",
-			baseURL: "https://api.intelligence.io.solutions/api/v1",
-			apiKey: options.ioIntelligenceApiKey,
-			modelId,
-			modelInfo,
-			modelMaxTokens: options.modelMaxTokens ?? undefined,
-			temperature: options.modelTemperature ?? 0.7,
-		}
-
-		super(options, config)
-	}
-
-	override getModel() {
-		const modelId = this.options.ioIntelligenceModelId ?? ioIntelligenceDefaultModelId
-		const modelInfo: ModelInfo = ioIntelligenceModels[modelId as IOIntelligenceModelId] ??
-			ioIntelligenceModels[ioIntelligenceDefaultModelId] ?? {
-				maxTokens: 8192,
-				contextWindow: 128000,
-				supportsImages: false,
-				supportsPromptCache: false,
-			}
-
-		const params = getModelParams({
-			format: "openai",
-			modelId,
-			model: modelInfo,
-			settings: this.options,
-			defaultTemperature: 0.7,
-		})
-
-		return { id: modelId, info: modelInfo, ...params }
-	}
-}

+ 0 - 208
src/api/providers/unbound.ts

@@ -1,208 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-import OpenAI from "openai"
-
-import { unboundDefaultModelId, unboundDefaultModelInfo } from "@roo-code/types"
-
-import type { ApiHandlerOptions } from "../../shared/api"
-
-import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
-import { convertToOpenAiMessages } from "../transform/openai-format"
-import { addCacheBreakpoints as addAnthropicCacheBreakpoints } from "../transform/caching/anthropic"
-import { addCacheBreakpoints as addGeminiCacheBreakpoints } from "../transform/caching/gemini"
-import { addCacheBreakpoints as addVertexCacheBreakpoints } from "../transform/caching/vertex"
-
-import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
-import { RouterProvider } from "./router-provider"
-import { getModelParams } from "../transform/model-params"
-import { getModels } from "./fetchers/modelCache"
-
-const ORIGIN_APP = "roo-code"
-
-const DEFAULT_HEADERS = {
-	"X-Unbound-Metadata": JSON.stringify({ labels: [{ key: "app", value: "roo-code" }] }),
-}
-
-interface UnboundUsage extends OpenAI.CompletionUsage {
-	cache_creation_input_tokens?: number
-	cache_read_input_tokens?: number
-}
-
-type UnboundChatCompletionCreateParamsStreaming = OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
-	unbound_metadata: {
-		originApp: string
-		taskId?: string
-		mode?: string
-	}
-}
-
-type UnboundChatCompletionCreateParamsNonStreaming = OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming & {
-	unbound_metadata: {
-		originApp: string
-	}
-}
-
-export class UnboundHandler extends RouterProvider implements SingleCompletionHandler {
-	constructor(options: ApiHandlerOptions) {
-		super({
-			options,
-			name: "unbound",
-			baseURL: "https://api.getunbound.ai/v1",
-			apiKey: options.unboundApiKey,
-			modelId: options.unboundModelId,
-			defaultModelId: unboundDefaultModelId,
-			defaultModelInfo: unboundDefaultModelInfo,
-		})
-	}
-
-	public override async fetchModel() {
-		this.models = await getModels({ provider: this.name, apiKey: this.client.apiKey, baseUrl: this.client.baseURL })
-		return this.getModel()
-	}
-
-	override getModel() {
-		const requestedId = this.options.unboundModelId ?? unboundDefaultModelId
-		const modelExists = this.models[requestedId]
-		const id = modelExists ? requestedId : unboundDefaultModelId
-		const info = modelExists ? this.models[requestedId] : unboundDefaultModelInfo
-
-		const params = getModelParams({
-			format: "openai",
-			modelId: id,
-			model: info,
-			settings: this.options,
-			defaultTemperature: 0,
-		})
-
-		return { id, info, ...params }
-	}
-
-	override async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		// Ensure we have up-to-date model metadata
-		await this.fetchModel()
-		const { id: modelId, info } = this.getModel()
-
-		const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
-			{ role: "system", content: systemPrompt },
-			...convertToOpenAiMessages(messages),
-		]
-
-		if (info.supportsPromptCache) {
-			if (modelId.startsWith("google/")) {
-				addGeminiCacheBreakpoints(systemPrompt, openAiMessages)
-			} else if (modelId.startsWith("anthropic/")) {
-				addAnthropicCacheBreakpoints(systemPrompt, openAiMessages)
-			}
-		}
-		// Custom models from Vertex AI (no configuration) need to be handled differently.
-		if (modelId.startsWith("vertex-ai/google.") || modelId.startsWith("vertex-ai/anthropic.")) {
-			addVertexCacheBreakpoints(messages)
-		}
-
-		// Required by Anthropic; other providers default to max tokens allowed.
-		let maxTokens: number | undefined
-
-		if (modelId.startsWith("anthropic/")) {
-			maxTokens = info.maxTokens ?? undefined
-		}
-
-		const requestOptions: UnboundChatCompletionCreateParamsStreaming = {
-			model: modelId.split("/")[1],
-			max_tokens: maxTokens,
-			messages: openAiMessages,
-			stream: true,
-			stream_options: { include_usage: true },
-			unbound_metadata: {
-				originApp: ORIGIN_APP,
-				taskId: metadata?.taskId,
-				mode: metadata?.mode,
-			},
-			tools: this.convertToolsForOpenAI(metadata?.tools),
-			tool_choice: metadata?.tool_choice,
-			parallel_tool_calls: metadata?.parallelToolCalls ?? true,
-		}
-
-		if (this.supportsTemperature(modelId)) {
-			requestOptions.temperature = this.options.modelTemperature ?? 0
-		}
-
-		const { data: completion } = await this.client.chat.completions
-			.create(requestOptions, { headers: DEFAULT_HEADERS })
-			.withResponse()
-
-		for await (const chunk of completion) {
-			const delta = chunk.choices[0]?.delta
-			const usage = chunk.usage as UnboundUsage
-
-			if (delta?.content) {
-				yield { type: "text", text: delta.content }
-			}
-
-			// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
-			if (delta?.tool_calls) {
-				for (const toolCall of delta.tool_calls) {
-					yield {
-						type: "tool_call_partial",
-						index: toolCall.index,
-						id: toolCall.id,
-						name: toolCall.function?.name,
-						arguments: toolCall.function?.arguments,
-					}
-				}
-			}
-
-			if (usage) {
-				const usageData: ApiStreamUsageChunk = {
-					type: "usage",
-					inputTokens: usage.prompt_tokens || 0,
-					outputTokens: usage.completion_tokens || 0,
-				}
-
-				// Only add cache tokens if they exist.
-				if (usage.cache_creation_input_tokens) {
-					usageData.cacheWriteTokens = usage.cache_creation_input_tokens
-				}
-
-				if (usage.cache_read_input_tokens) {
-					usageData.cacheReadTokens = usage.cache_read_input_tokens
-				}
-
-				yield usageData
-			}
-		}
-	}
-
-	async completePrompt(prompt: string): Promise<string> {
-		const { id: modelId, info } = await this.fetchModel()
-
-		try {
-			const requestOptions: UnboundChatCompletionCreateParamsNonStreaming = {
-				model: modelId.split("/")[1],
-				messages: [{ role: "user", content: prompt }],
-				unbound_metadata: {
-					originApp: ORIGIN_APP,
-				},
-			}
-
-			if (this.supportsTemperature(modelId)) {
-				requestOptions.temperature = this.options.modelTemperature ?? 0
-			}
-
-			if (modelId.startsWith("anthropic/")) {
-				requestOptions.max_tokens = info.maxTokens
-			}
-
-			const response = await this.client.chat.completions.create(requestOptions, { headers: DEFAULT_HEADERS })
-			return response.choices[0]?.message.content || ""
-		} catch (error) {
-			if (error instanceof Error) {
-				throw new Error(`Unbound completion error: ${error.message}`)
-			}
-
-			throw error
-		}
-	}
-}

+ 3 - 3
src/api/transform/__tests__/ai-sdk.spec.ts

@@ -810,9 +810,9 @@ describe("AI SDK conversion utilities", () => {
 				lastError: { message: "Too Many Requests", status: 429 },
 			}
 
-			const result = handleAiSdkError(retryError, "Groq")
+			const result = handleAiSdkError(retryError, "SambaNova")
 
-			expect(result.message).toContain("Groq:")
+			expect(result.message).toContain("SambaNova:")
 			expect(result.message).toContain("429")
 			expect((result as any).status).toBe(429)
 		})
@@ -833,7 +833,7 @@ describe("AI SDK conversion utilities", () => {
 
 		it("should preserve original error as cause", () => {
 			const originalError = new Error("Original error")
-			const result = handleAiSdkError(originalError, "Cerebras")
+			const result = handleAiSdkError(originalError, "Mistral")
 
 			expect((result as any).cause).toBe(originalError)
 		})

+ 14 - 7
src/core/config/ContextProxy.ts

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

+ 42 - 8
src/core/config/ProviderSettingsManager.ts

@@ -12,6 +12,7 @@ import {
 	getModelId,
 	type ProviderName,
 	isProviderName,
+	isRetiredProvider,
 } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 
@@ -359,8 +360,14 @@ 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 (including legacy
+				// provider-specific keys) to avoid data loss — passthrough() keeps
+				// unknown keys that strict parse() would strip.
+				const filteredConfig =
+					typeof config.apiProvider === "string" && isRetiredProvider(config.apiProvider)
+						? providerSettingsWithIdSchema.passthrough().parse(config)
+						: discriminatedProviderSettingsWithIdSchema.parse(config)
 				providerProfiles.apiConfigs[name] = { ...filteredConfig, id }
 				await this.store(providerProfiles)
 				return id
@@ -507,7 +514,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
@@ -582,7 +596,21 @@ export class ProviderSettingsManager {
 					// First, sanitize invalid apiProvider values before parsing
 					// This handles removed providers (like "glama") gracefully
 					const sanitizedConfig = this.sanitizeProviderConfig(apiConfig)
-					const result = providerSettingsWithIdSchema.safeParse(sanitizedConfig)
+
+					// For retired providers, use passthrough() to preserve legacy
+					// provider-specific fields (e.g. groqApiKey, deepInfraModelId)
+					// that strict parse() would strip.
+					const providerValue =
+						typeof sanitizedConfig === "object" &&
+						sanitizedConfig !== null &&
+						"apiProvider" in sanitizedConfig
+							? (sanitizedConfig as Record<string, unknown>).apiProvider
+							: undefined
+					const schema =
+						typeof providerValue === "string" && isRetiredProvider(providerValue)
+							? providerSettingsWithIdSchema.passthrough()
+							: providerSettingsWithIdSchema
+					const result = schema.safeParse(sanitizedConfig)
 					return result.success ? { ...acc, [key]: result.data } : acc
 				},
 				{} as Record<string, ProviderSettingsWithId>,
@@ -607,7 +635,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 +647,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

+ 52 - 6
src/core/config/__tests__/ContextProxy.spec.ts

@@ -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")

+ 124 - 9
src/core/config/__tests__/ProviderSettingsManager.spec.ts

@@ -566,6 +566,47 @@ describe("ProviderSettingsManager", () => {
 				"Failed to save config: Error: Failed to write provider profiles to secrets: Error: Storage failed",
 			)
 		})
+
+		it("should preserve full fields including legacy provider-specific keys when saving retired provider profiles", async () => {
+			mockSecrets.get.mockResolvedValue(
+				JSON.stringify({
+					currentApiConfigName: "default",
+					apiConfigs: {
+						default: {},
+					},
+					modeApiConfigs: {
+						code: "default",
+						architect: "default",
+						ask: "default",
+					},
+				}),
+			)
+
+			// Include a legacy provider-specific field (groqApiKey) that is no
+			// longer in the schema — passthrough() must keep it.
+			const retiredConfig = {
+				apiProvider: "groq",
+				apiKey: "legacy-key",
+				apiModelId: "legacy-model",
+				openAiBaseUrl: "https://legacy.example/v1",
+				openAiApiKey: "legacy-openai-key",
+				modelMaxTokens: 4096,
+				groqApiKey: "legacy-groq-specific-key",
+			} as ProviderSettings
+
+			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)
+			// Verify legacy provider-specific field is preserved via passthrough
+			expect(storedConfig.apiConfigs.retired.groqApiKey).toBe("legacy-groq-specific-key")
+			expect(storedConfig.apiConfigs.retired.id).toBeTruthy()
+		})
 	})
 
 	describe("DeleteConfig", () => {
@@ -695,9 +736,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 +747,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 +763,7 @@ describe("ProviderSettingsManager", () => {
 				},
 			}
 
-			mockSecrets.get.mockResolvedValue(JSON.stringify(configWithRemovedProvider))
+			mockSecrets.get.mockResolvedValue(JSON.stringify(configWithUnknownProvider))
 
 			await providerSettingsManager.initialize()
 
@@ -735,11 +776,55 @@ 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 including legacy provider-specific keys 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,
+						// Legacy provider-specific field no longer in schema
+						groqApiKey: "legacy-groq-key",
+					},
+				},
+				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)
+			// Verify legacy provider-specific field is preserved via passthrough
+			expect(storedConfig.apiConfigs.retiredProvider.groqApiKey).toBe("legacy-groq-key")
 		})
 
 		it("should sanitize invalid providers and remove non-object profiles during load", async () => {
@@ -791,6 +876,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

+ 0 - 38
src/core/context/context-management/__tests__/context-error-handling.test.ts

@@ -193,37 +193,6 @@ describe("checkContextWindowExceededError", () => {
 		})
 	})
 
-	describe("Cerebras errors", () => {
-		it("should detect Cerebras context window error", () => {
-			const error = {
-				status: 400,
-				message: "Please reduce the length of the messages or completion",
-			}
-
-			expect(checkContextWindowExceededError(error)).toBe(true)
-		})
-
-		it("should detect Cerebras error with nested structure", () => {
-			const error = {
-				error: {
-					status: 400,
-					message: "Please reduce the length of the messages or completion",
-				},
-			}
-
-			expect(checkContextWindowExceededError(error)).toBe(true)
-		})
-
-		it("should not detect non-context Cerebras errors", () => {
-			const error = {
-				status: 400,
-				message: "Invalid request parameters",
-			}
-
-			expect(checkContextWindowExceededError(error)).toBe(false)
-		})
-	})
-
 	describe("Edge cases", () => {
 		it("should handle null input", () => {
 			expect(checkContextWindowExceededError(null)).toBe(false)
@@ -317,13 +286,6 @@ describe("checkContextWindowExceededError", () => {
 				},
 			}
 			expect(checkContextWindowExceededError(error2)).toBe(true)
-
-			// This error should be detected by Cerebras check
-			const error3 = {
-				status: 400,
-				message: "Please reduce the length of the messages or completion",
-			}
-			expect(checkContextWindowExceededError(error3)).toBe(true)
 		})
 	})
 })

+ 1 - 20
src/core/context/context-management/context-error-handling.ts

@@ -4,8 +4,7 @@ export function checkContextWindowExceededError(error: unknown): boolean {
 	return (
 		checkIsOpenAIContextWindowError(error) ||
 		checkIsOpenRouterContextWindowError(error) ||
-		checkIsAnthropicContextWindowError(error) ||
-		checkIsCerebrasContextWindowError(error)
+		checkIsAnthropicContextWindowError(error)
 	)
 }
 
@@ -94,21 +93,3 @@ function checkIsAnthropicContextWindowError(response: unknown): boolean {
 		return false
 	}
 }
-
-function checkIsCerebrasContextWindowError(response: unknown): boolean {
-	try {
-		// Type guard to safely access properties
-		if (!response || typeof response !== "object") {
-			return false
-		}
-
-		// Use type assertions with proper checks
-		const res = response as Record<string, any>
-		const status = res.status ?? res.code ?? res.error?.status ?? res.response?.status
-		const message: string = String(res.message || res.error?.message || "")
-
-		return String(status) === "400" && message.includes("Please reduce the length of the messages or completion")
-	} catch {
-		return false
-	}
-}

+ 21 - 4
src/core/task/Task.ts

@@ -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
@@ -2730,7 +2735,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
@@ -2851,7 +2860,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"
@@ -3175,7 +3188,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 =

+ 9 - 3
src/core/webview/ClineProvider.ts

@@ -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"
@@ -2283,8 +2284,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()
@@ -3124,12 +3128,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,

+ 0 - 46
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -2577,7 +2577,6 @@ describe("ClineProvider - Router Models", () => {
 			apiConfiguration: {
 				openRouterApiKey: "openrouter-key",
 				requestyApiKey: "requesty-key",
-				unboundApiKey: "unbound-key",
 				litellmApiKey: "litellm-key",
 				litellmBaseUrl: "http://localhost:4000",
 			},
@@ -2606,9 +2605,7 @@ describe("ClineProvider - Router Models", () => {
 		// Verify getModels was called for each provider with correct options
 		expect(getModels).toHaveBeenCalledWith({ provider: "openrouter" })
 		expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
-		expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
 		expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
-		expect(getModels).toHaveBeenCalledWith({ provider: "deepinfra" })
 		expect(getModels).toHaveBeenCalledWith(
 			expect.objectContaining({
 				provider: "roo",
@@ -2620,24 +2617,18 @@ describe("ClineProvider - Router Models", () => {
 			apiKey: "litellm-key",
 			baseUrl: "http://localhost:4000",
 		})
-		expect(getModels).toHaveBeenCalledWith({ provider: "chutes" })
 
 		// Verify response was sent
 		expect(mockPostMessage).toHaveBeenCalledWith({
 			type: "routerModels",
 			routerModels: {
-				deepinfra: mockModels,
 				openrouter: mockModels,
 				requesty: mockModels,
-				unbound: mockModels,
 				roo: mockModels,
-				chutes: mockModels,
 				litellm: mockModels,
 				ollama: {},
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
-				huggingface: {},
-				"io-intelligence": {},
 			},
 			values: undefined,
 		})
@@ -2651,7 +2642,6 @@ describe("ClineProvider - Router Models", () => {
 			apiConfiguration: {
 				openRouterApiKey: "openrouter-key",
 				requestyApiKey: "requesty-key",
-				unboundApiKey: "unbound-key",
 				litellmApiKey: "litellm-key",
 				litellmBaseUrl: "http://localhost:4000",
 			},
@@ -2666,11 +2656,8 @@ describe("ClineProvider - Router Models", () => {
 		vi.mocked(getModels)
 			.mockResolvedValueOnce(mockModels) // openrouter success
 			.mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail
-			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
 			.mockResolvedValueOnce(mockModels) // vercel-ai-gateway success
-			.mockResolvedValueOnce(mockModels) // deepinfra success
 			.mockResolvedValueOnce(mockModels) // roo success
-			.mockRejectedValueOnce(new Error("Chutes API error")) // chutes fail
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
 
 		await messageHandler({ type: "requestRouterModels" })
@@ -2679,18 +2666,13 @@ describe("ClineProvider - Router Models", () => {
 		expect(mockPostMessage).toHaveBeenCalledWith({
 			type: "routerModels",
 			routerModels: {
-				deepinfra: mockModels,
 				openrouter: mockModels,
 				requesty: {},
-				unbound: {},
 				roo: mockModels,
-				chutes: {},
 				ollama: {},
 				lmstudio: {},
 				litellm: {},
 				"vercel-ai-gateway": mockModels,
-				huggingface: {},
-				"io-intelligence": {},
 			},
 			values: undefined,
 		})
@@ -2703,27 +2685,6 @@ describe("ClineProvider - Router Models", () => {
 			values: { provider: "requesty" },
 		})
 
-		expect(mockPostMessage).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "Unbound API error",
-			values: { provider: "unbound" },
-		})
-
-		expect(mockPostMessage).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "Unbound API error",
-			values: { provider: "unbound" },
-		})
-
-		expect(mockPostMessage).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "Chutes API error",
-			values: { provider: "chutes" },
-		})
-
 		expect(mockPostMessage).toHaveBeenCalledWith({
 			type: "singleRouterModelFetchResponse",
 			success: false,
@@ -2741,7 +2702,6 @@ describe("ClineProvider - Router Models", () => {
 			apiConfiguration: {
 				openRouterApiKey: "openrouter-key",
 				requestyApiKey: "requesty-key",
-				unboundApiKey: "unbound-key",
 				// No litellm config
 			},
 		} as any)
@@ -2776,7 +2736,6 @@ describe("ClineProvider - Router Models", () => {
 			apiConfiguration: {
 				openRouterApiKey: "openrouter-key",
 				requestyApiKey: "requesty-key",
-				unboundApiKey: "unbound-key",
 				// No litellm config
 			},
 		} as any)
@@ -2800,18 +2759,13 @@ describe("ClineProvider - Router Models", () => {
 		expect(mockPostMessage).toHaveBeenCalledWith({
 			type: "routerModels",
 			routerModels: {
-				deepinfra: mockModels,
 				openrouter: mockModels,
 				requesty: mockModels,
-				unbound: mockModels,
 				roo: mockModels,
-				chutes: mockModels,
 				litellm: {},
 				ollama: {},
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
-				huggingface: {},
-				"io-intelligence": {},
 			},
 			values: undefined,
 		})

+ 0 - 6
src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts

@@ -74,14 +74,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => {
 					return { "openrouter/qwen2.5": { contextWindow: 32768, supportsPromptCache: false } }
 				case "requesty":
 					return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } }
-				case "deepinfra":
-					return { "deepinfra/model": { contextWindow: 8192, supportsPromptCache: false } }
-				case "unbound":
-					return { "unbound/model": { contextWindow: 8192, supportsPromptCache: false } }
 				case "vercel-ai-gateway":
 					return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } }
-				case "io-intelligence":
-					return { "io/model": { contextWindow: 8192, supportsPromptCache: false } }
 				case "litellm":
 					return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } }
 				default:

+ 0 - 63
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -265,7 +265,6 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			apiConfiguration: {
 				openRouterApiKey: "openrouter-key",
 				requestyApiKey: "requesty-key",
-				unboundApiKey: "unbound-key",
 				litellmApiKey: "litellm-key",
 				litellmBaseUrl: "http://localhost:4000",
 			},
@@ -297,9 +296,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 		// Verify getModels was called for each provider
 		expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter" })
 		expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
-		expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
 		expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
-		expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" })
 		expect(mockGetModels).toHaveBeenCalledWith(
 			expect.objectContaining({
 				provider: "roo",
@@ -311,25 +308,18 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			apiKey: "litellm-key",
 			baseUrl: "http://localhost:4000",
 		})
-		// Note: huggingface is not fetched in requestRouterModels - it has its own handler
-		// Note: io-intelligence is not fetched because no API key is provided in the mock state
 
 		// Verify response was sent
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "routerModels",
 			routerModels: {
-				deepinfra: mockModels,
 				openrouter: mockModels,
 				requesty: mockModels,
-				unbound: mockModels,
 				litellm: mockModels,
 				roo: mockModels,
-				chutes: mockModels,
 				ollama: {},
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
-				huggingface: {},
-				"io-intelligence": {},
 			},
 			values: undefined,
 		})
@@ -340,7 +330,6 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			apiConfiguration: {
 				openRouterApiKey: "openrouter-key",
 				requestyApiKey: "requesty-key",
-				unboundApiKey: "unbound-key",
 				// Missing litellm config
 			},
 		})
@@ -377,7 +366,6 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			apiConfiguration: {
 				openRouterApiKey: "openrouter-key",
 				requestyApiKey: "requesty-key",
-				unboundApiKey: "unbound-key",
 				// Missing litellm config
 			},
 		})
@@ -409,18 +397,13 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "routerModels",
 			routerModels: {
-				deepinfra: mockModels,
 				openrouter: mockModels,
 				requesty: mockModels,
-				unbound: mockModels,
 				roo: mockModels,
-				chutes: mockModels,
 				litellm: {},
 				ollama: {},
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
-				huggingface: {},
-				"io-intelligence": {},
 			},
 			values: undefined,
 		})
@@ -440,11 +423,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 		mockGetModels
 			.mockResolvedValueOnce(mockModels) // openrouter
 			.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
-			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
 			.mockResolvedValueOnce(mockModels) // vercel-ai-gateway
-			.mockResolvedValueOnce(mockModels) // deepinfra
 			.mockResolvedValueOnce(mockModels) // roo
-			.mockRejectedValueOnce(new Error("Chutes API error")) // chutes
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {
@@ -459,20 +439,6 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			values: { provider: "requesty" },
 		})
 
-		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "Unbound API error",
-			values: { provider: "unbound" },
-		})
-
-		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "Chutes API error",
-			values: { provider: "chutes" },
-		})
-
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "singleRouterModelFetchResponse",
 			success: false,
@@ -484,18 +450,13 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "routerModels",
 			routerModels: {
-				deepinfra: mockModels,
 				openrouter: mockModels,
 				requesty: {},
-				unbound: {},
 				roo: mockModels,
-				chutes: {},
 				litellm: {},
 				ollama: {},
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
-				huggingface: {},
-				"io-intelligence": {},
 			},
 			values: undefined,
 		})
@@ -506,11 +467,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 		mockGetModels
 			.mockRejectedValueOnce(new Error("Structured error message")) // openrouter
 			.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
-			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
 			.mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway
-			.mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra
 			.mockRejectedValueOnce(new Error("Roo API error")) // roo
-			.mockRejectedValueOnce(new Error("Chutes API error")) // chutes
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {
@@ -532,20 +490,6 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			values: { provider: "requesty" },
 		})
 
-		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "Unbound API error",
-			values: { provider: "unbound" },
-		})
-
-		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "DeepInfra API error",
-			values: { provider: "deepinfra" },
-		})
-
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "singleRouterModelFetchResponse",
 			success: false,
@@ -560,13 +504,6 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			values: { provider: "roo" },
 		})
 
-		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
-			type: "singleRouterModelFetchResponse",
-			success: false,
-			error: "Chutes API error",
-			values: { provider: "chutes" },
-		})
-
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "singleRouterModelFetchResponse",
 			success: false,

+ 0 - 41
src/core/webview/webviewMessageHandler.ts

@@ -874,16 +874,11 @@ export const webviewMessageHandler = async (
 				: {
 						openrouter: {},
 						"vercel-ai-gateway": {},
-						huggingface: {},
 						litellm: {},
-						deepinfra: {},
-						"io-intelligence": {},
 						requesty: {},
-						unbound: {},
 						ollama: {},
 						lmstudio: {},
 						roo: {},
-						chutes: {},
 					}
 
 			const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
@@ -910,16 +905,7 @@ export const webviewMessageHandler = async (
 						baseUrl: apiConfiguration.requestyBaseUrl,
 					},
 				},
-				{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
 				{ key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } },
-				{
-					key: "deepinfra",
-					options: {
-						provider: "deepinfra",
-						apiKey: apiConfiguration.deepInfraApiKey,
-						baseUrl: apiConfiguration.deepInfraBaseUrl,
-					},
-				},
 				{
 					key: "roo",
 					options: {
@@ -930,20 +916,8 @@ export const webviewMessageHandler = async (
 							: undefined,
 					},
 				},
-				{
-					key: "chutes",
-					options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey },
-				},
 			]
 
-			// IO Intelligence is conditional on api key
-			if (apiConfiguration.ioIntelligenceApiKey) {
-				candidates.push({
-					key: "io-intelligence",
-					options: { provider: "io-intelligence", apiKey: apiConfiguration.ioIntelligenceApiKey },
-				})
-			}
-
 			// LiteLLM is conditional on baseUrl+apiKey
 			const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
 			const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
@@ -1131,21 +1105,6 @@ export const webviewMessageHandler = async (
 			// TODO: Cache like we do for OpenRouter, etc?
 			provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
 			break
-		case "requestHuggingFaceModels":
-			// TODO: Why isn't this handled by `requestRouterModels` above?
-			try {
-				const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface")
-				const huggingFaceModelsResponse = await getHuggingFaceModelsWithMetadata()
-
-				provider.postMessageToWebview({
-					type: "huggingFaceModels",
-					huggingFaceModels: huggingFaceModelsResponse.models,
-				})
-			} catch (error) {
-				console.error("Failed to fetch Hugging Face models:", error)
-				provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: [] })
-			}
-			break
 		case "openImage":
 			openImage(message.text!, { values: message.values })
 			break

+ 1 - 13
src/i18n/locales/ca/common.json

@@ -114,15 +114,6 @@
 			"thinking_complete_safety": "(Pensament completat, però la sortida s'ha bloquejat a causa de la configuració de seguretat.)",
 			"thinking_complete_recitation": "(Pensament completat, però la sortida s'ha bloquejat a causa de la comprovació de recitació.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Ha fallat l'autenticació de l'API de Cerebras. Comproveu que la vostra clau d'API sigui vàlida i no hagi caducat.",
-			"accessForbidden": "Accés denegat a l'API de Cerebras. La vostra clau d'API pot no tenir accés al model o funcionalitat sol·licitats.",
-			"rateLimitExceeded": "S'ha superat el límit de velocitat de l'API de Cerebras. Espereu abans de fer una altra sol·licitud.",
-			"serverError": "Error del servidor de l'API de Cerebras ({{status}}). Torneu-ho a provar més tard.",
-			"genericError": "Error de l'API de Cerebras ({{status}}): {{message}}",
-			"noResponseBody": "Error de l'API de Cerebras: No hi ha cos de resposta",
-			"completionError": "Error de finalització de Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "El proveïdor Roo requereix autenticació al núvol. Si us plau, inicieu sessió a Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"enter_valid_path": "Introdueix una ruta vàlida"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Clau API de Groq",
-			"getGroqApiKey": "Obté la clau API de Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/de/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Denken abgeschlossen, aber die Ausgabe wurde aufgrund von Sicherheitseinstellungen blockiert.)",
 			"thinking_complete_recitation": "(Denken abgeschlossen, aber die Ausgabe wurde aufgrund der Rezitationsprüfung blockiert.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API-Authentifizierung fehlgeschlagen. Bitte überprüfe, ob dein API-Schlüssel gültig und nicht abgelaufen ist.",
-			"accessForbidden": "Cerebras API-Zugriff verweigert. Dein API-Schlüssel hat möglicherweise keinen Zugriff auf das angeforderte Modell oder die Funktion.",
-			"rateLimitExceeded": "Cerebras API-Ratenlimit überschritten. Bitte warte, bevor du eine weitere Anfrage stellst.",
-			"serverError": "Cerebras API-Serverfehler ({{status}}). Bitte versuche es später erneut.",
-			"genericError": "Cerebras API-Fehler ({{status}}): {{message}}",
-			"noResponseBody": "Cerebras API-Fehler: Kein Antworttext vorhanden",
-			"completionError": "Cerebras-Vervollständigungsfehler: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo-Anbieter erfordert Cloud-Authentifizierung. Bitte melde dich bei Roo Code Cloud an."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Gib deine Aufgabe hier ein"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Groq API-Schlüssel",
-			"getGroqApiKey": "Groq API-Schlüssel erhalten"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 0 - 9
src/i18n/locales/en/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Thinking complete, but output was blocked due to safety settings.)",
 			"thinking_complete_recitation": "(Thinking complete, but output was blocked due to recitation check.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API authentication failed. Please check your API key is valid and not expired.",
-			"accessForbidden": "Cerebras API access forbidden. Your API key may not have access to the requested model or feature.",
-			"rateLimitExceeded": "Cerebras API rate limit exceeded. Please wait before making another request.",
-			"serverError": "Cerebras API server error ({{status}}). Please try again later.",
-			"genericError": "Cerebras API Error ({{status}}): {{message}}",
-			"noResponseBody": "Cerebras API Error: No response body",
-			"completionError": "Cerebras completion error: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo provider requires cloud authentication. Please sign in to Roo Code Cloud."
 		},

+ 1 - 13
src/i18n/locales/es/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Pensamiento completado, pero la salida fue bloqueada debido a la configuración de seguridad.)",
 			"thinking_complete_recitation": "(Pensamiento completado, pero la salida fue bloqueada debido a la comprobación de recitación.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Falló la autenticación de la API de Cerebras. Verifica que tu clave de API sea válida y no haya expirado.",
-			"accessForbidden": "Acceso prohibido a la API de Cerebras. Tu clave de API puede no tener acceso al modelo o función solicitada.",
-			"rateLimitExceeded": "Se excedió el límite de velocidad de la API de Cerebras. Espera antes de hacer otra solicitud.",
-			"serverError": "Error del servidor de la API de Cerebras ({{status}}). Inténtalo de nuevo más tarde.",
-			"genericError": "Error de la API de Cerebras ({{status}}): {{message}}",
-			"noResponseBody": "Error de la API de Cerebras: Sin cuerpo de respuesta",
-			"completionError": "Error de finalización de Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "El proveedor Roo requiere autenticación en la nube. Por favor, inicia sesión en Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Escribe tu tarea aquí"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Clave API de Groq",
-			"getGroqApiKey": "Obtener clave API de Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/fr/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Réflexion terminée, mais la sortie a été bloquée en raison des paramètres de sécurité.)",
 			"thinking_complete_recitation": "(Réflexion terminée, mais la sortie a été bloquée en raison de la vérification de récitation.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Échec de l'authentification de l'API Cerebras. Vérifiez que votre clé API est valide et n'a pas expiré.",
-			"accessForbidden": "Accès interdit à l'API Cerebras. Votre clé API peut ne pas avoir accès au modèle ou à la fonction demandée.",
-			"rateLimitExceeded": "Limite de débit de l'API Cerebras dépassée. Veuillez attendre avant de faire une autre demande.",
-			"serverError": "Erreur du serveur de l'API Cerebras ({{status}}). Veuillez réessayer plus tard.",
-			"genericError": "Erreur de l'API Cerebras ({{status}}) : {{message}}",
-			"noResponseBody": "Erreur de l'API Cerebras : Aucun corps de réponse",
-			"completionError": "Erreur d'achèvement de Cerebras : {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Le fournisseur Roo nécessite une authentification cloud. Veuillez vous connecter à Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Écris ta tâche ici"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Clé API Groq",
-			"getGroqApiKey": "Obtenir la clé API Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/hi/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(सोचना पूरा हुआ, लेकिन सुरक्षा सेटिंग्स के कारण आउटपुट अवरुद्ध कर दिया गया।)",
 			"thinking_complete_recitation": "(सोचना पूरा हुआ, लेकिन पाठ जाँच के कारण आउटपुट अवरुद्ध कर दिया गया।)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API प्रमाणीकरण विफल हुआ। कृपया जांचें कि आपकी API कुंजी वैध है और समाप्त नहीं हुई है।",
-			"accessForbidden": "Cerebras API पहुंच निषेध। आपकी API कुंजी का अनुरोधित मॉडल या सुविधा तक पहुंच नहीं हो सकती है।",
-			"rateLimitExceeded": "Cerebras API दर सीमा पार हो गई। कृपया दूसरा अनुरोध करने से पहले प्रतीक्षा करें।",
-			"serverError": "Cerebras API सर्वर त्रुटि ({{status}})। कृपया बाद में पुनः प्रयास करें।",
-			"genericError": "Cerebras API त्रुटि ({{status}}): {{message}}",
-			"noResponseBody": "Cerebras API त्रुटि: कोई प्रतिक्रिया मुख्य भाग नहीं",
-			"completionError": "Cerebras पूर्णता त्रुटि: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo प्रदाता को क्लाउड प्रमाणीकरण की आवश्यकता है। कृपया Roo Code Cloud में साइन इन करें।"
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "अपना कार्य यहाँ लिखें"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "ग्रोक एपीआई कुंजी",
-			"getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/id/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Berpikir selesai, tetapi output diblokir karena pengaturan keamanan.)",
 			"thinking_complete_recitation": "(Berpikir selesai, tetapi output diblokir karena pemeriksaan resitasi.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Autentikasi API Cerebras gagal. Silakan periksa apakah kunci API Anda valid dan belum kedaluwarsa.",
-			"accessForbidden": "Akses API Cerebras ditolak. Kunci API Anda mungkin tidak memiliki akses ke model atau fitur yang diminta.",
-			"rateLimitExceeded": "Batas kecepatan API Cerebras terlampaui. Silakan tunggu sebelum membuat permintaan lain.",
-			"serverError": "Kesalahan server API Cerebras ({{status}}). Silakan coba lagi nanti.",
-			"genericError": "Kesalahan API Cerebras ({{status}}): {{message}}",
-			"noResponseBody": "Kesalahan API Cerebras: Tidak ada isi respons",
-			"completionError": "Kesalahan penyelesaian Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Penyedia Roo memerlukan autentikasi cloud. Silakan masuk ke Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Ketik tugas kamu di sini"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Kunci API Groq",
-			"getGroqApiKey": "Dapatkan Kunci API Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/it/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Pensiero completato, ma l'output è stato bloccato a causa delle impostazioni di sicurezza.)",
 			"thinking_complete_recitation": "(Pensiero completato, ma l'output è stato bloccato a causa del controllo di recitazione.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Autenticazione API Cerebras fallita. Verifica che la tua chiave API sia valida e non scaduta.",
-			"accessForbidden": "Accesso API Cerebras negato. La tua chiave API potrebbe non avere accesso al modello o alla funzione richiesta.",
-			"rateLimitExceeded": "Limite di velocità API Cerebras superato. Attendi prima di fare un'altra richiesta.",
-			"serverError": "Errore del server API Cerebras ({{status}}). Riprova più tardi.",
-			"genericError": "Errore API Cerebras ({{status}}): {{message}}",
-			"noResponseBody": "Errore API Cerebras: Nessun corpo di risposta",
-			"completionError": "Errore di completamento Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Il provider Roo richiede l'autenticazione cloud. Accedi a Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Scrivi il tuo compito qui"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Chiave API Groq",
-			"getGroqApiKey": "Ottieni chiave API Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/ja/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(思考完了、安全設定により出力ブロック)",
 			"thinking_complete_recitation": "(思考完了、引用チェックにより出力ブロック)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API認証が失敗しました。APIキーが有効で期限切れではないことを確認してください。",
-			"accessForbidden": "Cerebras APIアクセスが禁止されています。あなたのAPIキーは要求されたモデルや機能にアクセスできない可能性があります。",
-			"rateLimitExceeded": "Cerebras APIレート制限を超過しました。別のリクエストを行う前にお待ちください。",
-			"serverError": "Cerebras APIサーバーエラー ({{status}})。しばらくしてからもう一度お試しください。",
-			"genericError": "Cerebras APIエラー ({{status}}): {{message}}",
-			"noResponseBody": "Cerebras APIエラー: レスポンスボディなし",
-			"completionError": "Cerebras完了エラー: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Rooプロバイダーはクラウド認証が必要です。Roo Code Cloudにサインインしてください。"
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "タスクをここに入力してください"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Groq APIキー",
-			"getGroqApiKey": "Groq APIキーを取得"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/ko/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(생각 완료, 안전 설정으로 출력 차단됨)",
 			"thinking_complete_recitation": "(생각 완료, 암송 확인으로 출력 차단됨)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API 인증에 실패했습니다. API 키가 유효하고 만료되지 않았는지 확인하세요.",
-			"accessForbidden": "Cerebras API 액세스가 금지되었습니다. API 키가 요청된 모델이나 기능에 액세스할 수 없을 수 있습니다.",
-			"rateLimitExceeded": "Cerebras API 속도 제한을 초과했습니다. 다른 요청을 하기 전에 기다리세요.",
-			"serverError": "Cerebras API 서버 오류 ({{status}}). 나중에 다시 시도하세요.",
-			"genericError": "Cerebras API 오류 ({{status}}): {{message}}",
-			"noResponseBody": "Cerebras API 오류: 응답 본문 없음",
-			"completionError": "Cerebras 완료 오류: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo 제공업체는 클라우드 인증이 필요합니다. Roo Code Cloud에 로그인하세요."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "여기에 작업을 입력하세요"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Groq API 키",
-			"getGroqApiKey": "Groq API 키 받기"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/nl/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Nadenken voltooid, maar uitvoer is geblokkeerd vanwege veiligheidsinstellingen.)",
 			"thinking_complete_recitation": "(Nadenken voltooid, maar uitvoer is geblokkeerd vanwege recitatiecontrole.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API-authenticatie mislukt. Controleer of je API-sleutel geldig is en niet verlopen.",
-			"accessForbidden": "Cerebras API-toegang geweigerd. Je API-sleutel heeft mogelijk geen toegang tot het gevraagde model of de functie.",
-			"rateLimitExceeded": "Cerebras API-snelheidslimiet overschreden. Wacht voordat je een ander verzoek doet.",
-			"serverError": "Cerebras API-serverfout ({{status}}). Probeer het later opnieuw.",
-			"genericError": "Cerebras API-fout ({{status}}): {{message}}",
-			"noResponseBody": "Cerebras API-fout: Geen responslichaam",
-			"completionError": "Cerebras-voltooiingsfout: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo provider vereist cloud authenticatie. Log in bij Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Typ hier je taak"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Groq API-sleutel",
-			"getGroqApiKey": "Groq API-sleutel ophalen"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/pl/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Myślenie zakończone, ale dane wyjściowe zostały zablokowane przez ustawienia bezpieczeństwa.)",
 			"thinking_complete_recitation": "(Myślenie zakończone, ale dane wyjściowe zostały zablokowane przez kontrolę recytacji.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Uwierzytelnianie API Cerebras nie powiodło się. Sprawdź, czy twój klucz API jest ważny i nie wygasł.",
-			"accessForbidden": "Dostęp do API Cerebras zabroniony. Twój klucz API może nie mieć dostępu do żądanego modelu lub funkcji.",
-			"rateLimitExceeded": "Przekroczono limit szybkości API Cerebras. Poczekaj przed wykonaniem kolejnego żądania.",
-			"serverError": "Błąd serwera API Cerebras ({{status}}). Spróbuj ponownie później.",
-			"genericError": "Błąd API Cerebras ({{status}}): {{message}}",
-			"noResponseBody": "Błąd API Cerebras: Brak treści odpowiedzi",
-			"completionError": "Błąd uzupełniania Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Dostawca Roo wymaga uwierzytelnienia w chmurze. Zaloguj się do Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Wpisz swoje zadanie tutaj"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Klucz API Groq",
-			"getGroqApiKey": "Uzyskaj klucz API Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/pt-BR/common.json

@@ -115,15 +115,6 @@
 			"thinking_complete_safety": "(Pensamento concluído, mas a saída foi bloqueada devido às configurações de segurança.)",
 			"thinking_complete_recitation": "(Pensamento concluído, mas a saída foi bloqueada devido à verificação de recitação.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Falha na autenticação da API Cerebras. Verifique se sua chave de API é válida e não expirou.",
-			"accessForbidden": "Acesso à API Cerebras negado. Sua chave de API pode não ter acesso ao modelo ou recurso solicitado.",
-			"rateLimitExceeded": "Limite de taxa da API Cerebras excedido. Aguarde antes de fazer outra solicitação.",
-			"serverError": "Erro do servidor da API Cerebras ({{status}}). Tente novamente mais tarde.",
-			"genericError": "Erro da API Cerebras ({{status}}): {{message}}",
-			"noResponseBody": "Erro da API Cerebras: Sem corpo de resposta",
-			"completionError": "Erro de conclusão do Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "O provedor Roo requer autenticação na nuvem. Faça login no Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"enter_valid_path": "Por favor, digite um caminho válido"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Chave de API Groq",
-			"getGroqApiKey": "Obter chave de API Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/ru/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Размышление завершено, но вывод заблокирован настройками безопасности.)",
 			"thinking_complete_recitation": "(Размышление завершено, но вывод заблокирован проверкой цитирования.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Ошибка аутентификации Cerebras API. Убедитесь, что ваш API-ключ действителен и не истек.",
-			"accessForbidden": "Доступ к Cerebras API запрещен. Ваш API-ключ может не иметь доступа к запрашиваемой модели или функции.",
-			"rateLimitExceeded": "Превышен лимит скорости Cerebras API. Подождите перед отправкой следующего запроса.",
-			"serverError": "Ошибка сервера Cerebras API ({{status}}). Попробуйте позже.",
-			"genericError": "Ошибка Cerebras API ({{status}}): {{message}}",
-			"noResponseBody": "Ошибка Cerebras API: Нет тела ответа",
-			"completionError": "Ошибка завершения Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Провайдер Roo требует облачной аутентификации. Войдите в Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Введите вашу задачу здесь"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Ключ API Groq",
-			"getGroqApiKey": "Получить ключ API Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/tr/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Düşünme tamamlandı, ancak çıktı güvenlik ayarları nedeniyle engellendi.)",
 			"thinking_complete_recitation": "(Düşünme tamamlandı, ancak çıktı okuma kontrolü nedeniyle engellendi.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API kimlik doğrulama başarısız oldu. API anahtarınızın geçerli olduğunu ve süresi dolmadığını kontrol edin.",
-			"accessForbidden": "Cerebras API erişimi yasak. API anahtarınız istenen modele veya özelliğe erişimi olmayabilir.",
-			"rateLimitExceeded": "Cerebras API hız sınırı aşıldı. Başka bir istek yapmadan önce bekleyin.",
-			"serverError": "Cerebras API sunucu hatası ({{status}}). Lütfen daha sonra tekrar deneyin.",
-			"genericError": "Cerebras API Hatası ({{status}}): {{message}}",
-			"noResponseBody": "Cerebras API Hatası: Yanıt gövdesi yok",
-			"completionError": "Cerebras tamamlama hatası: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo sağlayıcısı bulut kimlik doğrulaması gerektirir. Lütfen Roo Code Cloud'a giriş yapın."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Görevini buraya yaz"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Groq API Anahtarı",
-			"getGroqApiKey": "Groq API Anahtarı Al"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/vi/common.json

@@ -111,15 +111,6 @@
 			"thinking_complete_safety": "(Đã suy nghĩ xong nhưng kết quả bị chặn do cài đặt an toàn.)",
 			"thinking_complete_recitation": "(Đã suy nghĩ xong nhưng kết quả bị chặn do kiểm tra trích dẫn.)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Xác thực API Cerebras thất bại. Vui lòng kiểm tra khóa API của bạn có hợp lệ và chưa hết hạn.",
-			"accessForbidden": "Truy cập API Cerebras bị từ chối. Khóa API của bạn có thể không có quyền truy cập vào mô hình hoặc tính năng được yêu cầu.",
-			"rateLimitExceeded": "Vượt quá giới hạn tốc độ API Cerebras. Vui lòng chờ trước khi thực hiện yêu cầu khác.",
-			"serverError": "Lỗi máy chủ API Cerebras ({{status}}). Vui lòng thử lại sau.",
-			"genericError": "Lỗi API Cerebras ({{status}}): {{message}}",
-			"noResponseBody": "Lỗi API Cerebras: Không có nội dung phản hồi",
-			"completionError": "Lỗi hoàn thành Cerebras: {{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Nhà cung cấp Roo yêu cầu xác thực đám mây. Vui lòng đăng nhập vào Roo Code Cloud."
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "Nhập nhiệm vụ của bạn ở đây"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Khóa API Groq",
-			"getGroqApiKey": "Lấy khóa API Groq"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/zh-CN/common.json

@@ -116,15 +116,6 @@
 			"thinking_complete_safety": "(思考完成,但由于安全设置输出被阻止。)",
 			"thinking_complete_recitation": "(思考完成,但由于引用检查输出被阻止。)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API 身份验证失败。请检查你的 API 密钥是否有效且未过期。",
-			"accessForbidden": "Cerebras API 访问被禁止。你的 API 密钥可能无法访问请求的模型或功能。",
-			"rateLimitExceeded": "Cerebras API 速率限制已超出。请稍等后再发起另一个请求。",
-			"serverError": "Cerebras API 服务器错误 ({{status}})。请稍后重试。",
-			"genericError": "Cerebras API 错误 ({{status}}):{{message}}",
-			"noResponseBody": "Cerebras API 错误:无响应主体",
-			"completionError": "Cerebras 完成错误:{{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo 提供商需要云认证。请登录 Roo Code Cloud。"
 		},
@@ -210,10 +201,7 @@
 		"task_placeholder": "在这里输入任务"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Groq API 密钥",
-			"getGroqApiKey": "获取 Groq API 密钥"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 1 - 13
src/i18n/locales/zh-TW/common.json

@@ -110,15 +110,6 @@
 			"thinking_complete_safety": "(思考完成,但由於安全設定輸出被阻止。)",
 			"thinking_complete_recitation": "(思考完成,但由於引用檢查輸出被阻止。)"
 		},
-		"cerebras": {
-			"authenticationFailed": "Cerebras API 驗證失敗。請檢查您的 API 金鑰是否有效且未過期。",
-			"accessForbidden": "Cerebras API 存取被拒絕。您的 API 金鑰可能無法存取所請求的模型或功能。",
-			"rateLimitExceeded": "Cerebras API 速率限制已超出。請稍候再發出另一個請求。",
-			"serverError": "Cerebras API 伺服器錯誤 ({{status}})。請稍後重試。",
-			"genericError": "Cerebras API 錯誤 ({{status}}):{{message}}",
-			"noResponseBody": "Cerebras API 錯誤:無回應主體",
-			"completionError": "Cerebras 完成錯誤:{{error}}"
-		},
 		"roo": {
 			"authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。"
 		},
@@ -205,10 +196,7 @@
 		"task_placeholder": "在這裡輸入工作"
 	},
 	"settings": {
-		"providers": {
-			"groqApiKey": "Groq API 金鑰",
-			"getGroqApiKey": "取得 Groq API 金鑰"
-		}
+		"providers": {}
 	},
 	"customModes": {
 		"errors": {

+ 0 - 2
src/package.json

@@ -452,12 +452,10 @@
 	"dependencies": {
 		"@ai-sdk/amazon-bedrock": "^4.0.51",
 		"@ai-sdk/baseten": "^1.0.31",
-		"@ai-sdk/cerebras": "^2.0.31",
 		"@ai-sdk/deepseek": "^2.0.18",
 		"@ai-sdk/fireworks": "^2.0.32",
 		"@ai-sdk/google": "^3.0.22",
 		"@ai-sdk/google-vertex": "^4.0.45",
-		"@ai-sdk/groq": "^3.0.22",
 		"@ai-sdk/mistral": "^3.0.19",
 		"@ai-sdk/xai": "^3.0.48",
 		"@anthropic-ai/sdk": "^0.37.0",

+ 0 - 9
src/shared/ProfileValidator.ts

@@ -61,16 +61,11 @@ export class ProfileValidator {
 			case "mistral":
 			case "deepseek":
 			case "xai":
-			case "groq":
 			case "sambanova":
-			case "chutes":
 			case "fireworks":
-			case "featherless":
 				return profile.apiModelId
 			case "litellm":
 				return profile.litellmModelId
-			case "unbound":
-				return profile.unboundModelId
 			case "lmstudio":
 				return profile.lmStudioModelId
 			case "vscode-lm":
@@ -82,10 +77,6 @@ export class ProfileValidator {
 				return profile.ollamaModelId
 			case "requesty":
 				return profile.requestyModelId
-			case "io-intelligence":
-				return profile.ioIntelligenceModelId
-			case "deepinfra":
-				return profile.deepInfraModelId
 			case "fake-ai":
 			default:
 				return undefined

+ 0 - 34
src/shared/__tests__/ProfileValidator.spec.ts

@@ -176,11 +176,8 @@ describe("ProfileValidator", () => {
 			"mistral",
 			"deepseek",
 			"xai",
-			"groq",
-			"chutes",
 			"sambanova",
 			"fireworks",
-			"featherless",
 		]
 
 		apiModelProviders.forEach((provider) => {
@@ -216,22 +213,6 @@ describe("ProfileValidator", () => {
 			expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true)
 		})
 
-		// Test for io-intelligence provider which uses ioIntelligenceModelId
-		it(`should extract ioIntelligenceModelId for io-intelligence provider`, () => {
-			const allowList: OrganizationAllowList = {
-				allowAll: false,
-				providers: {
-					"io-intelligence": { allowAll: false, models: ["test-model"] },
-				},
-			}
-			const profile: ProviderSettings = {
-				apiProvider: "io-intelligence" as any,
-				ioIntelligenceModelId: "test-model",
-			}
-
-			expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true)
-		})
-
 		it("should extract vsCodeLmModelSelector.id for vscode-lm provider", () => {
 			const allowList: OrganizationAllowList = {
 				allowAll: false,
@@ -247,21 +228,6 @@ describe("ProfileValidator", () => {
 			expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true)
 		})
 
-		it("should extract unboundModelId for unbound provider", () => {
-			const allowList: OrganizationAllowList = {
-				allowAll: false,
-				providers: {
-					unbound: { allowAll: false, models: ["unbound-model"] },
-				},
-			}
-			const profile: ProviderSettings = {
-				apiProvider: "unbound",
-				unboundModelId: "unbound-model",
-			}
-
-			expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true)
-		})
-
 		it("should extract lmStudioModelId for lmstudio provider", () => {
 			const allowList: OrganizationAllowList = {
 				allowAll: false,

+ 0 - 1
src/shared/__tests__/checkExistApiConfig.spec.ts

@@ -55,7 +55,6 @@ describe("checkExistKey", () => {
 			mistralApiKey: undefined,
 			vsCodeLmModelSelector: undefined,
 			requestyApiKey: undefined,
-			unboundApiKey: undefined,
 		}
 		expect(checkExistKey(config)).toBe(false)
 	})

+ 0 - 5
src/shared/api.ts

@@ -171,16 +171,11 @@ type CommonFetchParams = {
 const dynamicProviderExtras = {
 	openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
 	"vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
-	huggingface: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
 	litellm: {} as { apiKey: string; baseUrl: string },
-	deepinfra: {} as { apiKey?: string; baseUrl?: string },
-	"io-intelligence": {} as { apiKey: string },
 	requesty: {} as { apiKey?: string; baseUrl?: string },
-	unbound: {} as { apiKey?: string },
 	ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
 	lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
 	roo: {} as { apiKey?: string; baseUrl?: string },
-	chutes: {} as { apiKey?: string },
 } as const satisfies Record<RouterName, object>
 
 // Build the dynamic options union from the map, intersected with CommonFetchParams

+ 40 - 2
webview-ui/src/components/chat/ChatView.tsx

@@ -14,6 +14,7 @@ import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting"
 import { batchConsecutive } from "@src/utils/batchConsecutive"
 
 import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types"
+import { isRetiredProvider } from "@roo-code/types"
 
 import { findLast } from "@roo/array"
 import { SuggestionItem } from "@roo-code/types"
@@ -40,6 +41,7 @@ import Announcement from "./Announcement"
 import BrowserActionRow from "./BrowserActionRow"
 import BrowserSessionStatusRow from "./BrowserSessionStatusRow"
 import ChatRow from "./ChatRow"
+import WarningRow from "./WarningRow"
 import { ChatTextArea } from "./ChatTextArea"
 import TaskHeader from "./TaskHeader"
 import SystemPromptWarning from "./SystemPromptWarning"
@@ -99,6 +101,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		showWorktreesInHomeScreen,
 	} = useExtensionState()
 
+	// Show a WarningRow when the user sends a message with a retired provider.
+	const [showRetiredProviderWarning, setShowRetiredProviderWarning] = useState(false)
+
+	// When the provider changes, clear the retired-provider warning.
+	const providerName = apiConfiguration?.apiProvider
+	useEffect(() => {
+		setShowRetiredProviderWarning(false)
+	}, [providerName])
+
 	const messagesRef = useRef(messages)
 
 	useEffect(() => {
@@ -626,6 +637,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 			text = text.trim()
 
 			if (text || images.length > 0) {
+				// Intercept when the active provider is retired — show a
+				// WarningRow instead of sending anything to the backend.
+				if (apiConfiguration?.apiProvider && isRetiredProvider(apiConfiguration.apiProvider)) {
+					setShowRetiredProviderWarning(true)
+					return
+				}
+
 				// Queue message if:
 				// - Task is busy (sendingDisabled)
 				// - API request in progress (isStreaming)
@@ -691,7 +709,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				handleChatReset()
 			}
 		},
-		[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length], // messagesRef and clineAskRef are stable
+		[
+			handleChatReset,
+			markFollowUpAsAnswered,
+			sendingDisabled,
+			isStreaming,
+			messageQueue.length,
+			apiConfiguration?.apiProvider,
+		], // messagesRef and clineAskRef are stable
 	)
 
 	const handleSetChatBoxMessage = useCallback(
@@ -709,7 +734,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		[inputValue, selectedImages],
 	)
 
-	const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), [])
+	const startNewTask = useCallback(() => {
+		setShowRetiredProviderWarning(false)
+		vscode.postMessage({ type: "clearTask" })
+	}, [])
 
 	// Handle stop button click from textarea
 	const handleStopTask = useCallback(() => {
@@ -1834,6 +1862,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					}
 				}}
 			/>
+			{showRetiredProviderWarning && (
+				<div className="px-[15px] py-1">
+					<WarningRow
+						title={t("chat:retiredProvider.title")}
+						message={t("chat:retiredProvider.message")}
+						actionText={t("chat:retiredProvider.openSettings")}
+						onAction={() => vscode.postMessage({ type: "switchTab", tab: "settings" })}
+					/>
+				</div>
+			)}
 			<ChatTextArea
 				ref={textAreaRef}
 				inputValue={inputValue}

+ 335 - 375
webview-ui/src/components/settings/ApiOptions.tsx

@@ -7,24 +7,20 @@ import { ExternalLinkIcon } from "@radix-ui/react-icons"
 import {
 	type ProviderName,
 	type ProviderSettings,
+	isRetiredProvider,
 	DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
 	openRouterDefaultModelId,
 	requestyDefaultModelId,
-	unboundDefaultModelId,
 	litellmDefaultModelId,
 	openAiNativeDefaultModelId,
 	openAiCodexDefaultModelId,
 	anthropicDefaultModelId,
-	doubaoDefaultModelId,
 	qwenCodeDefaultModelId,
 	geminiDefaultModelId,
 	deepSeekDefaultModelId,
 	moonshotDefaultModelId,
 	mistralDefaultModelId,
 	xaiDefaultModelId,
-	groqDefaultModelId,
-	cerebrasDefaultModelId,
-	chutesDefaultModelId,
 	basetenDefaultModelId,
 	bedrockDefaultModelId,
 	vertexDefaultModelId,
@@ -32,11 +28,8 @@ import {
 	internationalZAiDefaultModelId,
 	mainlandZAiDefaultModelId,
 	fireworksDefaultModelId,
-	featherlessDefaultModelId,
-	ioIntelligenceDefaultModelId,
 	rooDefaultModelId,
 	vercelAiGatewayDefaultModelId,
-	deepInfraDefaultModelId,
 	minimaxDefaultModelId,
 } from "@roo-code/types"
 
@@ -75,14 +68,8 @@ import {
 	Anthropic,
 	Baseten,
 	Bedrock,
-	Cerebras,
-	Chutes,
 	DeepSeek,
-	Doubao,
 	Gemini,
-	Groq,
-	HuggingFace,
-	IOIntelligence,
 	LMStudio,
 	LiteLLM,
 	Mistral,
@@ -96,15 +83,12 @@ import {
 	Requesty,
 	Roo,
 	SambaNova,
-	Unbound,
 	Vertex,
 	VSCodeLM,
 	XAI,
 	ZAi,
 	Fireworks,
-	Featherless,
 	VercelAiGateway,
-	DeepInfra,
 	MiniMax,
 } from "./providers"
 
@@ -196,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()
 
@@ -213,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.
@@ -243,11 +236,7 @@ const ApiOptions = ({
 				vscode.postMessage({ type: "requestLmStudioModels" })
 			} else if (selectedProvider === "vscode-lm") {
 				vscode.postMessage({ type: "requestVsCodeLmModels" })
-			} else if (
-				selectedProvider === "litellm" ||
-				selectedProvider === "deepinfra" ||
-				selectedProvider === "roo"
-			) {
+			} else if (selectedProvider === "litellm" || selectedProvider === "roo") {
 				vscode.postMessage({ type: "requestRouterModels" })
 			}
 		},
@@ -261,20 +250,23 @@ const ApiOptions = ({
 			apiConfiguration?.lmStudioBaseUrl,
 			apiConfiguration?.litellmBaseUrl,
 			apiConfiguration?.litellmApiKey,
-			apiConfiguration?.deepInfraApiKey,
-			apiConfiguration?.deepInfraBaseUrl,
 			customHeaders,
 		],
 	)
 
 	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) => {
@@ -282,7 +274,7 @@ const ApiOptions = ({
 
 			// It would be much easier to have a single attribute that stores
 			// the modelId, but we have a separate attribute for each of
-			// OpenRouter, Unbound, and Requesty.
+			// OpenRouter and Requesty.
 			// If you switch to one of these providers and the corresponding
 			// modelId is not set then you immediately end up in an error state.
 			// To address that we set the modelId to the default value for th
@@ -336,25 +328,19 @@ const ApiOptions = ({
 					}
 				>
 			> = {
-				deepinfra: { field: "deepInfraModelId", default: deepInfraDefaultModelId },
 				openrouter: { field: "openRouterModelId", default: openRouterDefaultModelId },
-				unbound: { field: "unboundModelId", default: unboundDefaultModelId },
 				requesty: { field: "requestyModelId", default: requestyDefaultModelId },
 				litellm: { field: "litellmModelId", default: litellmDefaultModelId },
 				anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
-				cerebras: { field: "apiModelId", default: cerebrasDefaultModelId },
 				"openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId },
 				"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
 				"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
 				gemini: { field: "apiModelId", default: geminiDefaultModelId },
 				deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
-				doubao: { field: "apiModelId", default: doubaoDefaultModelId },
 				moonshot: { field: "apiModelId", default: moonshotDefaultModelId },
 				minimax: { field: "apiModelId", default: minimaxDefaultModelId },
 				mistral: { field: "apiModelId", default: mistralDefaultModelId },
 				xai: { field: "apiModelId", default: xaiDefaultModelId },
-				groq: { field: "apiModelId", default: groqDefaultModelId },
-				chutes: { field: "apiModelId", default: chutesDefaultModelId },
 				baseten: { field: "apiModelId", default: basetenDefaultModelId },
 				bedrock: { field: "apiModelId", default: bedrockDefaultModelId },
 				vertex: { field: "apiModelId", default: vertexDefaultModelId },
@@ -367,8 +353,6 @@ const ApiOptions = ({
 							: internationalZAiDefaultModelId,
 				},
 				fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
-				featherless: { field: "apiModelId", default: featherlessDefaultModelId },
-				"io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId },
 				roo: { field: "apiModelId", default: rooDefaultModelId },
 				"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
 				openai: { field: "openAiModelId" },
@@ -500,379 +484,355 @@ 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 === "unbound" && (
-				<Unbound
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					routerModels={routerModels}
-					organizationAllowList={organizationAllowList}
-					modelValidationError={modelValidationError}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
-
-			{selectedProvider === "deepinfra" && (
-				<DeepInfra
-					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} />
-			)}
+			{isRetiredSelectedProvider ? (
+				<div
+					className="rounded-md border border-vscode-panel-border px-3 py-2 text-sm text-vscode-descriptionForeground"
+					data-testid="retired-provider-message">
+					{t("settings:providers.retiredProviderMessage")}
+				</div>
+			) : (
+				<>
+					{selectedProvider === "openrouter" && (
+						<OpenRouter
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							routerModels={routerModels}
+							selectedModelId={selectedModelId}
+							uriScheme={uriScheme}
+							simplifySettings={fromWelcomeView}
+							organizationAllowList={organizationAllowList}
+							modelValidationError={modelValidationError}
+						/>
+					)}
 
-			{selectedProvider === "gemini" && (
-				<Gemini apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "requesty" && (
+						<Requesty
+							uriScheme={uriScheme}
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							routerModels={routerModels}
+							refetchRouterModels={refetchRouterModels}
+							organizationAllowList={organizationAllowList}
+							modelValidationError={modelValidationError}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "openai" && (
-				<OpenAICompatible
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					organizationAllowList={organizationAllowList}
-					modelValidationError={modelValidationError}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "anthropic" && (
+						<Anthropic
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "lmstudio" && (
-				<LMStudio apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "openai-codex" && (
+						<OpenAICodex
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							simplifySettings={fromWelcomeView}
+							openAiCodexIsAuthenticated={openAiCodexIsAuthenticated}
+						/>
+					)}
 
-			{selectedProvider === "deepseek" && (
-				<DeepSeek
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "openai-native" && (
+						<OpenAI
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							selectedModelInfo={selectedModelInfo}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "doubao" && (
-				<Doubao
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "mistral" && (
+						<Mistral
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "qwen-code" && (
-				<QwenCode
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "baseten" && (
+						<Baseten
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "moonshot" && (
-				<Moonshot
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "bedrock" && (
+						<Bedrock
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							selectedModelInfo={selectedModelInfo}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "minimax" && (
-				<MiniMax apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "vertex" && (
+						<Vertex
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{selectedProvider === "vscode-lm" && (
-				<VSCodeLM apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "gemini" && (
+						<Gemini
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{selectedProvider === "ollama" && (
-				<Ollama apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "openai" && (
+						<OpenAICompatible
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							organizationAllowList={organizationAllowList}
+							modelValidationError={modelValidationError}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "xai" && (
-				<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "lmstudio" && (
+						<LMStudio
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{selectedProvider === "groq" && (
-				<Groq apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "deepseek" && (
+						<DeepSeek
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "huggingface" && (
-				<HuggingFace apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "qwen-code" && (
+						<QwenCode
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "cerebras" && (
-				<Cerebras apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "moonshot" && (
+						<Moonshot
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "chutes" && (
-				<Chutes
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					routerModels={routerModels}
-					organizationAllowList={organizationAllowList}
-					modelValidationError={modelValidationError}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "minimax" && (
+						<MiniMax
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{selectedProvider === "litellm" && (
-				<LiteLLM
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					organizationAllowList={organizationAllowList}
-					modelValidationError={modelValidationError}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "vscode-lm" && (
+						<VSCodeLM
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{selectedProvider === "sambanova" && (
-				<SambaNova apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "ollama" && (
+						<Ollama
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{selectedProvider === "zai" && (
-				<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "xai" && (
+						<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
+					)}
 
-			{selectedProvider === "io-intelligence" && (
-				<IOIntelligence
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					organizationAllowList={organizationAllowList}
-					modelValidationError={modelValidationError}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "litellm" && (
+						<LiteLLM
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							organizationAllowList={organizationAllowList}
+							modelValidationError={modelValidationError}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "vercel-ai-gateway" && (
-				<VercelAiGateway
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					routerModels={routerModels}
-					organizationAllowList={organizationAllowList}
-					modelValidationError={modelValidationError}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "sambanova" && (
+						<SambaNova
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{selectedProvider === "fireworks" && (
-				<Fireworks apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "zai" && (
+						<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
+					)}
 
-			{selectedProvider === "roo" && (
-				<Roo
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					routerModels={routerModels}
-					cloudIsAuthenticated={cloudIsAuthenticated}
-					organizationAllowList={organizationAllowList}
-					modelValidationError={modelValidationError}
-					simplifySettings={fromWelcomeView}
-				/>
-			)}
+					{selectedProvider === "vercel-ai-gateway" && (
+						<VercelAiGateway
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							routerModels={routerModels}
+							organizationAllowList={organizationAllowList}
+							modelValidationError={modelValidationError}
+							simplifySettings={fromWelcomeView}
+						/>
+					)}
 
-			{selectedProvider === "featherless" && (
-				<Featherless apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
-			)}
+					{selectedProvider === "fireworks" && (
+						<Fireworks
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
+					)}
 
-			{/* Generic model picker for providers with static models */}
-			{shouldUseGenericModelPicker(selectedProvider) && (
-				<>
-					<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 === "bedrock" && selectedModelId === "custom-arn" && (
-						<BedrockCustomArn
+					{selectedProvider === "roo" && (
+						<Roo
 							apiConfiguration={apiConfiguration}
 							setApiConfigurationField={setApiConfigurationField}
+							routerModels={routerModels}
+							cloudIsAuthenticated={cloudIsAuthenticated}
+							organizationAllowList={organizationAllowList}
+							modelValidationError={modelValidationError}
+							simplifySettings={fromWelcomeView}
 						/>
 					)}
-				</>
-			)}
 
-			{!fromWelcomeView && (
-				<ThinkingBudget
-					key={`${selectedProvider}-${selectedModelId}`}
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					modelInfo={selectedModelInfo}
-				/>
-			)}
+					{/* 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,
+									)
+								}
+							/>
 
-			{/* Gate Verbosity UI by capability flag */}
-			{!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
-				<Verbosity
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					modelInfo={selectedModelInfo}
-				/>
-			)}
+							{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
+								<BedrockCustomArn
+									apiConfiguration={apiConfiguration}
+									setApiConfigurationField={setApiConfigurationField}
+								/>
+							)}
+						</>
+					)}
 
-			{!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)}
+					{!fromWelcomeView && (
+						<ThinkingBudget
+							key={`${selectedProvider}-${selectedModelId}`}
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							modelInfo={selectedModelInfo}
 						/>
-						<ConsecutiveMistakeLimitControl
-							value={
-								apiConfiguration.consecutiveMistakeLimit !== undefined
-									? apiConfiguration.consecutiveMistakeLimit
-									: DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
-							}
-							onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
+					)}
+
+					{/* Gate Verbosity UI by capability flag */}
+					{!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
+						<Verbosity
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+							modelInfo={selectedModelInfo}
 						/>
-						{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>
+					)}
+
+					{!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>
 	)

+ 8 - 6
webview-ui/src/components/settings/ModelPicker.tsx

@@ -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"
@@ -29,12 +29,9 @@ import { ApiErrorMessage } from "./ApiErrorMessage"
 type ModelIdKey = keyof Pick<
 	ProviderSettings,
 	| "openRouterModelId"
-	| "unboundModelId"
 	| "requestyModelId"
 	| "openAiModelId"
 	| "litellmModelId"
-	| "deepInfraModelId"
-	| "ioIntelligenceModelId"
 	| "vercelAiGatewayModelId"
 	| "apiModelId"
 	| "ollamaModelId"
@@ -107,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
@@ -128,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("")
 

+ 0 - 2
webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx

@@ -167,9 +167,7 @@ describe("ApiOptions Provider Filtering", () => {
 		expect(providerValues).toContain("ollama")
 		expect(providerValues).toContain("lmstudio")
 		expect(providerValues).toContain("litellm")
-		expect(providerValues).toContain("unbound")
 		expect(providerValues).toContain("requesty")
-		expect(providerValues).toContain("io-intelligence")
 	})
 
 	it("should filter static providers based on organization allow list", () => {

+ 27 - 0
webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx

@@ -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(
+			"settings:providers.retiredProviderMessage",
+		)
+		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")
+	})
 })

+ 0 - 17
webview-ui/src/components/settings/constants.ts

@@ -3,7 +3,6 @@ import {
 	type ModelInfo,
 	anthropicModels,
 	bedrockModels,
-	cerebrasModels,
 	deepSeekModels,
 	moonshotModels,
 	geminiModels,
@@ -13,12 +12,9 @@ import {
 	qwenCodeModels,
 	vertexModels,
 	xaiModels,
-	groqModels,
 	sambaNovaModels,
-	doubaoModels,
 	internationalZAiModels,
 	fireworksModels,
-	featherlessModels,
 	minimaxModels,
 	basetenModels,
 } from "@roo-code/types"
@@ -26,9 +22,7 @@ import {
 export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, ModelInfo>>> = {
 	anthropic: anthropicModels,
 	bedrock: bedrockModels,
-	cerebras: cerebrasModels,
 	deepseek: deepSeekModels,
-	doubao: doubaoModels,
 	moonshot: moonshotModels,
 	gemini: geminiModels,
 	mistral: mistralModels,
@@ -37,22 +31,17 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
 	"qwen-code": qwenCodeModels,
 	vertex: vertexModels,
 	xai: xaiModels,
-	groq: groqModels,
 	sambanova: sambaNovaModels,
 	zai: internationalZAiModels,
 	fireworks: fireworksModels,
-	featherless: featherlessModels,
 	minimax: minimaxModels,
 	baseten: basetenModels,
 }
 
 export const PROVIDERS = [
 	{ value: "openrouter", label: "OpenRouter", proxy: false },
-	{ value: "deepinfra", label: "DeepInfra", proxy: false },
 	{ value: "anthropic", label: "Anthropic", proxy: false },
-	{ value: "cerebras", label: "Cerebras", proxy: false },
 	{ value: "gemini", label: "Google Gemini", proxy: false },
-	{ value: "doubao", label: "Doubao", proxy: false },
 	{ value: "deepseek", label: "DeepSeek", proxy: false },
 	{ value: "moonshot", label: "Moonshot", proxy: false },
 	{ value: "openai-native", label: "OpenAI", proxy: false },
@@ -65,18 +54,12 @@ export const PROVIDERS = [
 	{ value: "mistral", label: "Mistral", proxy: false },
 	{ value: "lmstudio", label: "LM Studio", proxy: true },
 	{ value: "ollama", label: "Ollama", proxy: true },
-	{ value: "unbound", label: "Unbound", proxy: false },
 	{ value: "requesty", label: "Requesty", proxy: false },
 	{ value: "xai", label: "xAI (Grok)", proxy: false },
-	{ value: "groq", label: "Groq", proxy: false },
-	{ value: "huggingface", label: "Hugging Face", proxy: false },
-	{ value: "chutes", label: "Chutes AI", proxy: false },
 	{ value: "litellm", label: "LiteLLM", proxy: true },
 	{ value: "sambanova", label: "SambaNova", proxy: false },
 	{ value: "zai", label: "Z.ai", proxy: false },
 	{ value: "fireworks", label: "Fireworks AI", proxy: false },
-	{ value: "featherless", label: "Featherless AI", proxy: false },
-	{ value: "io-intelligence", label: "IO Intelligence", proxy: false },
 	{ value: "roo", label: "Roo Code Router", proxy: false },
 	{ value: "vercel-ai-gateway", label: "Vercel AI Gateway", proxy: false },
 	{ value: "minimax", label: "MiniMax", proxy: false },

+ 0 - 50
webview-ui/src/components/settings/providers/Cerebras.tsx

@@ -1,50 +0,0 @@
-import { useCallback } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import type { ProviderSettings } from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-
-import { inputEventTransform } from "../transforms"
-
-type CerebrasProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-}
-
-export const Cerebras = ({ apiConfiguration, setApiConfigurationField }: CerebrasProps) => {
-	const { t } = useAppTranslation()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.cerebrasApiKey || ""}
-				type="password"
-				onInput={handleInputChange("cerebrasApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.cerebrasApiKey")}</label>
-			</VSCodeTextField>
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-			{!apiConfiguration?.cerebrasApiKey && (
-				<VSCodeButtonLink href="https://cloud.cerebras.ai?utm_source=roocode" appearance="secondary">
-					{t("settings:providers.getCerebrasApiKey")}
-				</VSCodeButtonLink>
-			)}
-		</>
-	)
-}

+ 0 - 76
webview-ui/src/components/settings/providers/Chutes.tsx

@@ -1,76 +0,0 @@
-import { useCallback } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import type { ProviderSettings, OrganizationAllowList, RouterModels } from "@roo-code/types"
-import { chutesDefaultModelId } from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-
-import { ModelPicker } from "../ModelPicker"
-import { inputEventTransform } from "../transforms"
-
-type ChutesProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-	routerModels?: RouterModels
-	organizationAllowList: OrganizationAllowList
-	modelValidationError?: string
-	simplifySettings?: boolean
-}
-
-export const Chutes = ({
-	apiConfiguration,
-	setApiConfigurationField,
-	routerModels,
-	organizationAllowList,
-	modelValidationError,
-	simplifySettings,
-}: ChutesProps) => {
-	const { t } = useAppTranslation()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.chutesApiKey || ""}
-				type="password"
-				onInput={handleInputChange("chutesApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.chutesApiKey")}</label>
-			</VSCodeTextField>
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-			{!apiConfiguration?.chutesApiKey && (
-				<VSCodeButtonLink href="https://chutes.ai/app/api" appearance="secondary">
-					{t("settings:providers.getChutesApiKey")}
-				</VSCodeButtonLink>
-			)}
-
-			<ModelPicker
-				apiConfiguration={apiConfiguration}
-				setApiConfigurationField={setApiConfigurationField}
-				defaultModelId={chutesDefaultModelId}
-				models={routerModels?.chutes ?? {}}
-				modelIdKey="apiModelId"
-				serviceName="Chutes AI"
-				serviceUrl="https://llm.chutes.ai/v1/models"
-				organizationAllowList={organizationAllowList}
-				errorMessage={modelValidationError}
-				simplifySettings={simplifySettings}
-			/>
-		</>
-	)
-}

+ 0 - 100
webview-ui/src/components/settings/providers/DeepInfra.tsx

@@ -1,100 +0,0 @@
-import { useCallback, useEffect, useState } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import {
-	type OrganizationAllowList,
-	type ProviderSettings,
-	type RouterModels,
-	deepInfraDefaultModelId,
-} from "@roo-code/types"
-
-import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { Button } from "@src/components/ui"
-
-import { inputEventTransform } from "../transforms"
-import { ModelPicker } from "../ModelPicker"
-
-type DeepInfraProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-	routerModels?: RouterModels
-	refetchRouterModels: () => void
-	organizationAllowList: OrganizationAllowList
-	modelValidationError?: string
-	simplifySettings?: boolean
-}
-
-export const DeepInfra = ({
-	apiConfiguration,
-	setApiConfigurationField,
-	routerModels,
-	refetchRouterModels,
-	organizationAllowList,
-	modelValidationError,
-	simplifySettings,
-}: DeepInfraProps) => {
-	const { t } = useAppTranslation()
-
-	const [didRefetch, setDidRefetch] = useState<boolean>()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	useEffect(() => {
-		// When base URL or API key changes, trigger a silent refresh of models
-		// The outer ApiOptions debounces and sends requestRouterModels; this keeps UI responsive
-	}, [apiConfiguration.deepInfraBaseUrl, apiConfiguration.deepInfraApiKey])
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.deepInfraApiKey || ""}
-				type="password"
-				onInput={handleInputChange("deepInfraApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.apiKey")}</label>
-			</VSCodeTextField>
-
-			<Button
-				variant="outline"
-				onClick={() => {
-					vscode.postMessage({ type: "flushRouterModels", text: "deepinfra" })
-					refetchRouterModels()
-					setDidRefetch(true)
-				}}>
-				<div className="flex items-center gap-2">
-					<span className="codicon codicon-refresh" />
-					{t("settings:providers.refreshModels.label")}
-				</div>
-			</Button>
-			{didRefetch && (
-				<div className="flex items-center text-vscode-errorForeground">
-					{t("settings:providers.refreshModels.hint")}
-				</div>
-			)}
-
-			<ModelPicker
-				apiConfiguration={apiConfiguration}
-				setApiConfigurationField={setApiConfigurationField}
-				defaultModelId={deepInfraDefaultModelId}
-				models={routerModels?.deepinfra ?? {}}
-				modelIdKey="deepInfraModelId"
-				serviceName="Deep Infra"
-				serviceUrl="https://deepinfra.com/models"
-				organizationAllowList={organizationAllowList}
-				errorMessage={modelValidationError}
-				simplifySettings={simplifySettings}
-			/>
-		</>
-	)
-}

+ 0 - 53
webview-ui/src/components/settings/providers/Doubao.tsx

@@ -1,53 +0,0 @@
-import { useCallback } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import type { ProviderSettings } from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-
-import { inputEventTransform } from "../transforms"
-
-type DoubaoProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-	simplifySettings?: boolean
-}
-
-export const Doubao = ({ apiConfiguration, setApiConfigurationField }: DoubaoProps) => {
-	const { t } = useAppTranslation()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.doubaoApiKey || ""}
-				type="password"
-				onInput={handleInputChange("doubaoApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.doubaoApiKey")}</label>
-			</VSCodeTextField>
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-			{!apiConfiguration?.doubaoApiKey && (
-				<VSCodeButtonLink
-					href="https://www.volcengine.com/experience/ark?model=doubao-1-5-thinking-vision-pro-250428"
-					appearance="secondary">
-					{t("settings:providers.getDoubaoApiKey")}
-				</VSCodeButtonLink>
-			)}
-		</>
-	)
-}

+ 0 - 50
webview-ui/src/components/settings/providers/Featherless.tsx

@@ -1,50 +0,0 @@
-import { useCallback } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import type { ProviderSettings } from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-
-import { inputEventTransform } from "../transforms"
-
-type FeatherlessProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-}
-
-export const Featherless = ({ apiConfiguration, setApiConfigurationField }: FeatherlessProps) => {
-	const { t } = useAppTranslation()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.featherlessApiKey || ""}
-				type="password"
-				onInput={handleInputChange("featherlessApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.featherlessApiKey")}</label>
-			</VSCodeTextField>
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-			{!apiConfiguration?.featherlessApiKey && (
-				<VSCodeButtonLink href="https://featherless.ai/account/api-keys" appearance="secondary">
-					{t("settings:providers.getFeatherlessApiKey")}
-				</VSCodeButtonLink>
-			)}
-		</>
-	)
-}

+ 0 - 50
webview-ui/src/components/settings/providers/Groq.tsx

@@ -1,50 +0,0 @@
-import { useCallback } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import type { ProviderSettings } from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-
-import { inputEventTransform } from "../transforms"
-
-type GroqProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-}
-
-export const Groq = ({ apiConfiguration, setApiConfigurationField }: GroqProps) => {
-	const { t } = useAppTranslation()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.groqApiKey || ""}
-				type="password"
-				onInput={handleInputChange("groqApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.groqApiKey")}</label>
-			</VSCodeTextField>
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-			{!apiConfiguration?.groqApiKey && (
-				<VSCodeButtonLink href="https://console.groq.com/keys" appearance="secondary">
-					{t("settings:providers.getGroqApiKey")}
-				</VSCodeButtonLink>
-			)}
-		</>
-	)
-}

+ 0 - 277
webview-ui/src/components/settings/providers/HuggingFace.tsx

@@ -1,277 +0,0 @@
-import { useCallback, useState, useEffect, useMemo } from "react"
-import { useEvent } from "react-use"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import type { ProviderSettings, ExtensionMessage } from "@roo-code/types"
-
-import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-import { SearchableSelect, type SearchableSelectOption } from "@src/components/ui"
-import { cn } from "@src/lib/utils"
-import { formatPrice } from "@/utils/formatPrice"
-
-import { inputEventTransform } from "../transforms"
-
-type HuggingFaceModel = {
-	id: string
-	object: string
-	created: number
-	owned_by: string
-	providers: Array<{
-		provider: string
-		status: "live" | "staging" | "error"
-		supports_tools?: boolean
-		supports_structured_output?: boolean
-		context_length?: number
-		pricing?: {
-			input: number
-			output: number
-		}
-	}>
-}
-
-type HuggingFaceProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (
-		field: keyof ProviderSettings,
-		value: ProviderSettings[keyof ProviderSettings],
-		isUserAction?: boolean,
-	) => void
-}
-
-export const HuggingFace = ({ apiConfiguration, setApiConfigurationField }: HuggingFaceProps) => {
-	const { t } = useAppTranslation()
-	const [models, setModels] = useState<HuggingFaceModel[]>([])
-	const [loading, setLoading] = useState(false)
-	const [selectedProvider, setSelectedProvider] = useState<string>(
-		apiConfiguration?.huggingFaceInferenceProvider || "auto",
-	)
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	// Fetch models when component mounts.
-	useEffect(() => {
-		setLoading(true)
-		vscode.postMessage({ type: "requestHuggingFaceModels" })
-	}, [])
-
-	// Handle messages from extension.
-	const onMessage = useCallback((event: MessageEvent) => {
-		const message: ExtensionMessage = event.data
-
-		switch (message.type) {
-			case "huggingFaceModels":
-				setModels(message.huggingFaceModels?.sort((a, b) => a.id.localeCompare(b.id)) || [])
-				setLoading(false)
-				break
-		}
-	}, [])
-
-	useEvent("message", onMessage)
-
-	// Get current model and its providers
-	const currentModel = models.find((m) => m.id === apiConfiguration?.huggingFaceModelId)
-	const availableProviders = useMemo(() => currentModel?.providers || [], [currentModel?.providers])
-
-	// Set default provider when model changes
-	useEffect(() => {
-		if (currentModel && availableProviders.length > 0) {
-			const savedProvider = apiConfiguration?.huggingFaceInferenceProvider
-			if (savedProvider) {
-				// Use saved provider if it exists
-				setSelectedProvider(savedProvider)
-			} else {
-				const currentProvider = availableProviders.find((p) => p.provider === selectedProvider)
-				if (!currentProvider) {
-					// Set to "auto" as default
-					const defaultProvider = "auto"
-					setSelectedProvider(defaultProvider)
-					setApiConfigurationField("huggingFaceInferenceProvider", defaultProvider, false) // false = automatic default
-				}
-			}
-		}
-	}, [
-		currentModel,
-		availableProviders,
-		selectedProvider,
-		apiConfiguration?.huggingFaceInferenceProvider,
-		setApiConfigurationField,
-	])
-
-	const handleModelSelect = (modelId: string) => {
-		setApiConfigurationField("huggingFaceModelId", modelId)
-		// Reset provider selection when model changes
-		const defaultProvider = "auto"
-		setSelectedProvider(defaultProvider)
-		setApiConfigurationField("huggingFaceInferenceProvider", defaultProvider)
-	}
-
-	const handleProviderSelect = (provider: string) => {
-		setSelectedProvider(provider)
-		setApiConfigurationField("huggingFaceInferenceProvider", provider)
-	}
-
-	// Format provider name for display
-	const formatProviderName = (provider: string) => {
-		const nameMap: Record<string, string> = {
-			sambanova: "SambaNova",
-			"fireworks-ai": "Fireworks",
-			together: "Together AI",
-			nebius: "Nebius AI Studio",
-			hyperbolic: "Hyperbolic",
-			novita: "Novita",
-			cohere: "Cohere",
-			"hf-inference": "HF Inference API",
-			replicate: "Replicate",
-		}
-		return nameMap[provider] || provider.charAt(0).toUpperCase() + provider.slice(1)
-	}
-
-	// Get current provider
-	const currentProvider = useMemo(() => {
-		if (!currentModel || !selectedProvider || selectedProvider === "auto") return null
-		return currentModel.providers.find((p) => p.provider === selectedProvider)
-	}, [currentModel, selectedProvider])
-
-	// Get model capabilities based on current provider
-	const modelCapabilities = useMemo(() => {
-		if (!currentModel) return null
-
-		// For now, assume text-only models since we don't have pipeline_tag in new API
-		// This could be enhanced by checking model name patterns or adding vision support detection
-		const supportsImages = false
-
-		// Use provider-specific capabilities if a specific provider is selected
-		const maxTokens =
-			currentProvider?.context_length || currentModel.providers.find((p) => p.context_length)?.context_length
-		const supportsTools = currentProvider?.supports_tools || currentModel.providers.some((p) => p.supports_tools)
-
-		return {
-			supportsImages,
-			maxTokens,
-			supportsTools,
-		}
-	}, [currentModel, currentProvider])
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.huggingFaceApiKey || ""}
-				type="password"
-				onInput={handleInputChange("huggingFaceApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.huggingFaceApiKey")}</label>
-			</VSCodeTextField>
-
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-
-			{!apiConfiguration?.huggingFaceApiKey && (
-				<VSCodeButtonLink href="https://huggingface.co/settings/tokens" appearance="secondary">
-					{t("settings:providers.getHuggingFaceApiKey")}
-				</VSCodeButtonLink>
-			)}
-
-			<div className="flex flex-col gap-2">
-				<label className="block font-medium text-sm">
-					{t("settings:providers.huggingFaceModelId")}
-					{loading && (
-						<span className="text-xs text-gray-400 ml-2">{t("settings:providers.huggingFaceLoading")}</span>
-					)}
-					{!loading && (
-						<span className="text-xs text-gray-400 ml-2">
-							{t("settings:providers.huggingFaceModelsCount", { count: models.length })}
-						</span>
-					)}
-				</label>
-
-				<SearchableSelect
-					value={apiConfiguration?.huggingFaceModelId || ""}
-					onValueChange={handleModelSelect}
-					options={models.map(
-						(model): SearchableSelectOption => ({
-							value: model.id,
-							label: model.id,
-						}),
-					)}
-					placeholder={t("settings:providers.huggingFaceSelectModel")}
-					searchPlaceholder={t("settings:providers.huggingFaceSearchModels")}
-					emptyMessage={t("settings:providers.huggingFaceNoModelsFound")}
-					disabled={loading}
-				/>
-			</div>
-
-			{currentModel && availableProviders.length > 0 && (
-				<div className="flex flex-col gap-2">
-					<label className="block font-medium text-sm">{t("settings:providers.huggingFaceProvider")}</label>
-					<SearchableSelect
-						value={selectedProvider}
-						onValueChange={handleProviderSelect}
-						options={[
-							{ value: "auto", label: t("settings:providers.huggingFaceProviderAuto") },
-							...availableProviders.map(
-								(mapping): SearchableSelectOption => ({
-									value: mapping.provider,
-									label: `${formatProviderName(mapping.provider)} (${mapping.status})`,
-								}),
-							),
-						]}
-						placeholder={t("settings:providers.huggingFaceSelectProvider")}
-						searchPlaceholder={t("settings:providers.huggingFaceSearchProviders")}
-						emptyMessage={t("settings:providers.huggingFaceNoProvidersFound")}
-					/>
-				</div>
-			)}
-
-			{/* Model capabilities */}
-			{currentModel && modelCapabilities && (
-				<div className="text-sm text-vscode-descriptionForeground">
-					<div
-						className={cn(
-							"flex items-center gap-1 font-medium",
-							modelCapabilities.supportsImages
-								? "text-vscode-charts-green"
-								: "text-vscode-errorForeground",
-						)}>
-						<span
-							className={cn("codicon", modelCapabilities.supportsImages ? "codicon-check" : "codicon-x")}
-						/>
-						{modelCapabilities.supportsImages
-							? t("settings:modelInfo.supportsImages")
-							: t("settings:modelInfo.noImages")}
-					</div>
-					{modelCapabilities.maxTokens && (
-						<div>
-							<span className="font-medium">{t("settings:modelInfo.maxOutput")}:</span>{" "}
-							{modelCapabilities.maxTokens.toLocaleString()} tokens
-						</div>
-					)}
-					{currentProvider?.pricing && (
-						<>
-							<div>
-								<span className="font-medium">{t("settings:modelInfo.inputPrice")}:</span>{" "}
-								{formatPrice(currentProvider.pricing.input)} / 1M tokens
-							</div>
-							<div>
-								<span className="font-medium">{t("settings:modelInfo.outputPrice")}:</span>{" "}
-								{formatPrice(currentProvider.pricing.output)} / 1M tokens
-							</div>
-						</>
-					)}
-				</div>
-			)}
-		</>
-	)
-}

+ 0 - 80
webview-ui/src/components/settings/providers/IOIntelligence.tsx

@@ -1,80 +0,0 @@
-import { useCallback } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-
-import {
-	type ProviderSettings,
-	type OrganizationAllowList,
-	ioIntelligenceDefaultModelId,
-	ioIntelligenceModels,
-} from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-import { useExtensionState } from "@src/context/ExtensionStateContext"
-
-import { ModelPicker } from "../ModelPicker"
-
-import { inputEventTransform } from "../transforms"
-
-type IOIntelligenceProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-	organizationAllowList: OrganizationAllowList
-	modelValidationError?: string
-	simplifySettings?: boolean
-}
-
-export const IOIntelligence = ({
-	apiConfiguration,
-	setApiConfigurationField,
-	organizationAllowList,
-	modelValidationError,
-	simplifySettings,
-}: IOIntelligenceProps) => {
-	const { t } = useAppTranslation()
-	const { routerModels } = useExtensionState()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.ioIntelligenceApiKey || ""}
-				type="password"
-				onInput={handleInputChange("ioIntelligenceApiKey")}
-				placeholder={t("settings:providers.ioIntelligenceApiKeyPlaceholder")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.ioIntelligenceApiKey")}</label>
-			</VSCodeTextField>
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-			{!apiConfiguration?.ioIntelligenceApiKey && (
-				<VSCodeButtonLink href="https://ai.io.net/ai/api-keys" appearance="secondary">
-					{t("settings:providers.getIoIntelligenceApiKey")}
-				</VSCodeButtonLink>
-			)}
-			<ModelPicker
-				apiConfiguration={apiConfiguration}
-				defaultModelId={ioIntelligenceDefaultModelId}
-				models={routerModels?.["io-intelligence"] ?? ioIntelligenceModels}
-				modelIdKey="ioIntelligenceModelId"
-				serviceName="IO Intelligence"
-				serviceUrl="https://api.intelligence.io.solutions/api/v1/models"
-				setApiConfigurationField={setApiConfigurationField}
-				organizationAllowList={organizationAllowList}
-				errorMessage={modelValidationError}
-				simplifySettings={simplifySettings}
-			/>
-		</>
-	)
-}

+ 0 - 197
webview-ui/src/components/settings/providers/Unbound.tsx

@@ -1,197 +0,0 @@
-import { useCallback, useState, useRef } from "react"
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { useQueryClient } from "@tanstack/react-query"
-
-import {
-	type ProviderSettings,
-	type OrganizationAllowList,
-	type RouterModels,
-	unboundDefaultModelId,
-} from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-import { vscode } from "@src/utils/vscode"
-import { Button } from "@src/components/ui"
-
-import { inputEventTransform } from "../transforms"
-import { ModelPicker } from "../ModelPicker"
-
-type UnboundProps = {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (
-		field: keyof ProviderSettings,
-		value: ProviderSettings[keyof ProviderSettings],
-		isUserAction?: boolean,
-	) => void
-	routerModels?: RouterModels
-	organizationAllowList: OrganizationAllowList
-	modelValidationError?: string
-	simplifySettings?: boolean
-}
-
-export const Unbound = ({
-	apiConfiguration,
-	setApiConfigurationField,
-	routerModels,
-	organizationAllowList,
-	modelValidationError,
-	simplifySettings,
-}: UnboundProps) => {
-	const { t } = useAppTranslation()
-	const [didRefetch, setDidRefetch] = useState<boolean>()
-	const [isInvalidKey, setIsInvalidKey] = useState<boolean>(false)
-	const queryClient = useQueryClient()
-
-	// Add refs to store timer IDs
-	const didRefetchTimerRef = useRef<NodeJS.Timeout>()
-	const invalidKeyTimerRef = useRef<NodeJS.Timeout>()
-
-	const handleInputChange = useCallback(
-		<K extends keyof ProviderSettings, E>(
-			field: K,
-			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
-		) =>
-			(event: E | Event) => {
-				setApiConfigurationField(field, transform(event as E))
-			},
-		[setApiConfigurationField],
-	)
-
-	const saveConfiguration = useCallback(async () => {
-		vscode.postMessage({
-			type: "upsertApiConfiguration",
-			text: "default",
-			apiConfiguration: apiConfiguration,
-		})
-
-		const waitForStateUpdate = new Promise<void>((resolve, reject) => {
-			const timeoutId = setTimeout(() => {
-				window.removeEventListener("message", messageHandler)
-				reject(new Error("Timeout waiting for state update"))
-			}, 10000) // 10 second timeout
-
-			const messageHandler = (event: MessageEvent) => {
-				const message = event.data
-				if (message.type === "state") {
-					clearTimeout(timeoutId)
-					window.removeEventListener("message", messageHandler)
-					resolve()
-				}
-			}
-			window.addEventListener("message", messageHandler)
-		})
-
-		try {
-			await waitForStateUpdate
-		} catch (error) {
-			console.error("Failed to save configuration:", error)
-		}
-	}, [apiConfiguration])
-
-	const requestModels = useCallback(async () => {
-		vscode.postMessage({ type: "flushRouterModels", text: "unbound" })
-
-		const modelsPromise = new Promise<void>((resolve) => {
-			const messageHandler = (event: MessageEvent) => {
-				const message = event.data
-
-				if (message.type === "routerModels") {
-					window.removeEventListener("message", messageHandler)
-					resolve()
-				}
-			}
-
-			window.addEventListener("message", messageHandler)
-		})
-
-		vscode.postMessage({ type: "requestRouterModels" })
-
-		await modelsPromise
-
-		await queryClient.invalidateQueries({ queryKey: ["routerModels"] })
-
-		// After refreshing models, check if current model is in the updated list
-		// If not, select the first available model
-		const updatedModels = queryClient.getQueryData<{ unbound: RouterModels }>(["routerModels"])?.unbound
-		if (updatedModels && Object.keys(updatedModels).length > 0) {
-			const currentModelId = apiConfiguration?.unboundModelId
-			const modelExists = currentModelId && Object.prototype.hasOwnProperty.call(updatedModels, currentModelId)
-
-			if (!currentModelId || !modelExists) {
-				const firstAvailableModelId = Object.keys(updatedModels)[0]
-				setApiConfigurationField("unboundModelId", firstAvailableModelId, false) // false = automatic model selection
-			}
-		}
-
-		if (!updatedModels || Object.keys(updatedModels).includes("error")) {
-			return false
-		} else {
-			return true
-		}
-	}, [queryClient, apiConfiguration, setApiConfigurationField])
-
-	const handleRefresh = useCallback(async () => {
-		await saveConfiguration()
-		const requestModelsResult = await requestModels()
-
-		if (requestModelsResult) {
-			setDidRefetch(true)
-			didRefetchTimerRef.current = setTimeout(() => setDidRefetch(false), 3000)
-		} else {
-			setIsInvalidKey(true)
-			invalidKeyTimerRef.current = setTimeout(() => setIsInvalidKey(false), 3000)
-		}
-	}, [saveConfiguration, requestModels])
-
-	return (
-		<>
-			<VSCodeTextField
-				value={apiConfiguration?.unboundApiKey || ""}
-				type="password"
-				onInput={handleInputChange("unboundApiKey")}
-				placeholder={t("settings:placeholders.apiKey")}
-				className="w-full">
-				<label className="block font-medium mb-1">{t("settings:providers.unboundApiKey")}</label>
-			</VSCodeTextField>
-			<div className="text-sm text-vscode-descriptionForeground -mt-2">
-				{t("settings:providers.apiKeyStorageNotice")}
-			</div>
-			{!apiConfiguration?.unboundApiKey && (
-				<VSCodeButtonLink href="https://gateway.getunbound.ai" appearance="secondary">
-					{t("settings:providers.getUnboundApiKey")}
-				</VSCodeButtonLink>
-			)}
-			<div className="flex justify-end">
-				<Button variant="outline" onClick={handleRefresh} className="w-1/2 max-w-xs">
-					<div className="flex items-center gap-2 justify-center">
-						<span className="codicon codicon-refresh" />
-						{t("settings:providers.refreshModels.label")}
-					</div>
-				</Button>
-			</div>
-			{didRefetch && (
-				<div className="flex items-center text-vscode-charts-green">
-					{t("settings:providers.unboundRefreshModelsSuccess")}
-				</div>
-			)}
-			{isInvalidKey && (
-				<div className="flex items-center text-vscode-errorForeground">
-					{t("settings:providers.unboundInvalidApiKey")}
-				</div>
-			)}
-			<ModelPicker
-				apiConfiguration={apiConfiguration}
-				defaultModelId={unboundDefaultModelId}
-				models={routerModels?.unbound ?? {}}
-				modelIdKey="unboundModelId"
-				serviceName="Unbound"
-				serviceUrl="https://api.getunbound.ai/models"
-				setApiConfigurationField={setApiConfigurationField}
-				organizationAllowList={organizationAllowList}
-				errorMessage={modelValidationError}
-				simplifySettings={simplifySettings}
-			/>
-		</>
-	)
-}

+ 0 - 300
webview-ui/src/components/settings/providers/__tests__/HuggingFace.spec.tsx

@@ -1,300 +0,0 @@
-import { render, screen } from "@/utils/test-utils"
-import { HuggingFace } from "../HuggingFace"
-import { ProviderSettings } from "@roo-code/types"
-
-// Mock the VSCode components
-vi.mock("@vscode/webview-ui-toolkit/react", () => ({
-	VSCodeTextField: ({
-		children,
-		value,
-		onInput,
-		placeholder,
-		className,
-		style,
-		"data-testid": dataTestId,
-		...rest
-	}: any) => {
-		return (
-			<div
-				data-testid={dataTestId ? `${dataTestId}-text-field` : "vscode-text-field"}
-				className={className}
-				style={style}>
-				{children}
-				<input
-					type="text"
-					value={value}
-					onChange={(e) => onInput && onInput(e)}
-					placeholder={placeholder}
-					data-testid={dataTestId}
-					{...rest}
-				/>
-			</div>
-		)
-	},
-	VSCodeCheckbox: ({ children, checked, onChange, ...rest }: any) => (
-		<div data-testid="vscode-checkbox">
-			<input
-				type="checkbox"
-				checked={checked}
-				onChange={onChange}
-				data-testid="vscode-checkbox-input"
-				{...rest}
-			/>
-			{children}
-		</div>
-	),
-	VSCodeLink: ({ children, href, onClick }: any) => (
-		<a href={href} onClick={onClick} data-testid="vscode-link">
-			{children}
-		</a>
-	),
-	VSCodeButton: ({ children, onClick, ...rest }: any) => (
-		<button onClick={onClick} data-testid="vscode-button" {...rest}>
-			{children}
-		</button>
-	),
-}))
-
-// Mock the translation hook
-vi.mock("@src/i18n/TranslationContext", () => ({
-	useAppTranslation: () => ({
-		t: (key: string) => {
-			// Return the key for testing, but simulate some actual translations
-			const translations: Record<string, string> = {
-				"settings:providers.getHuggingFaceApiKey": "Get Hugging Face API Key",
-				"settings:providers.huggingFaceApiKey": "Hugging Face API Key",
-				"settings:providers.huggingFaceModelId": "Model ID",
-				"settings:modelInfo.fetchingModels": "Fetching models...",
-				"settings:modelInfo.errorFetchingModels": "Error fetching models",
-				"settings:modelInfo.noModelsFound": "No models found",
-				"settings:modelInfo.noImages": "Does not support images",
-			}
-			return translations[key] || key
-		},
-	}),
-}))
-
-// Mock the UI components
-vi.mock("@src/components/ui", () => ({
-	Button: ({ children, onClick, variant, ...rest }: any) => (
-		<button onClick={onClick} data-testid="button" data-variant={variant} {...rest}>
-			{children}
-		</button>
-	),
-	Select: ({ children }: any) => <div data-testid="select">{children}</div>,
-	SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
-	SelectItem: ({ children }: any) => <div data-testid="select-item">{children}</div>,
-	SelectTrigger: ({ children }: any) => <div data-testid="select-trigger">{children}</div>,
-	SelectValue: ({ placeholder }: any) => <div data-testid="select-value">{placeholder}</div>,
-	SearchableSelect: ({ value, onValueChange, placeholder, children }: any) => (
-		<div data-testid="searchable-select">
-			<input
-				data-testid="searchable-select-input"
-				value={value}
-				onChange={(e) => onValueChange && onValueChange(e.target.value)}
-				placeholder={placeholder}
-			/>
-			{children}
-		</div>
-	),
-}))
-
-// Mock the formatPrice utility
-vi.mock("@/utils/formatPrice", () => ({
-	formatPrice: (price: number) => `$${price.toFixed(2)}`,
-}))
-
-// Create a mock postMessage function
-const mockPostMessage = vi.fn()
-
-// Mock the vscode module
-vi.mock("@src/utils/vscode", () => ({
-	vscode: {
-		postMessage: vi.fn(),
-	},
-}))
-
-// Import the mocked module to set up the spy
-import { vscode } from "@src/utils/vscode"
-
-describe("HuggingFace Component", () => {
-	const mockSetApiConfigurationField = vi.fn()
-
-	beforeEach(() => {
-		vi.clearAllMocks()
-		// Set up the mock implementation
-		vi.mocked(vscode.postMessage).mockImplementation(mockPostMessage)
-	})
-
-	it("should render with internationalized labels", () => {
-		const apiConfiguration: Partial<ProviderSettings> = {
-			huggingFaceApiKey: "",
-			huggingFaceModelId: "",
-		}
-
-		render(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Check that the translated labels are rendered
-		expect(screen.getByText("Get Hugging Face API Key")).toBeInTheDocument()
-		expect(screen.getByText("Hugging Face API Key")).toBeInTheDocument()
-		expect(screen.getByText("Model ID")).toBeInTheDocument()
-	})
-
-	it("should render API key input field", () => {
-		const apiConfiguration: Partial<ProviderSettings> = {
-			huggingFaceApiKey: "test-api-key",
-			huggingFaceModelId: "",
-		}
-
-		render(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Check that the API key input is rendered with the correct value
-		const apiKeyInput = screen.getByDisplayValue("test-api-key")
-		expect(apiKeyInput).toBeInTheDocument()
-	})
-
-	it("should render model selection components", () => {
-		const apiConfiguration: Partial<ProviderSettings> = {
-			huggingFaceApiKey: "test-api-key",
-			huggingFaceModelId: "test-model",
-		}
-
-		render(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Check that the searchable select component is rendered
-		expect(screen.getByTestId("searchable-select")).toBeInTheDocument()
-		expect(screen.getByTestId("searchable-select-input")).toBeInTheDocument()
-	})
-
-	it("should display the get API key link", () => {
-		const apiConfiguration: Partial<ProviderSettings> = {
-			huggingFaceApiKey: "",
-			huggingFaceModelId: "",
-		}
-
-		render(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Check that the API key button is rendered
-		const apiKeyButton = screen.getByTestId("button")
-		expect(apiKeyButton).toBeInTheDocument()
-		expect(apiKeyButton).toHaveTextContent("Get Hugging Face API Key")
-	})
-
-	it("should fetch models when component mounts", () => {
-		const apiConfiguration: Partial<ProviderSettings> = {
-			huggingFaceApiKey: "test-api-key",
-			huggingFaceModelId: "",
-		}
-
-		render(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Check that the fetch models message was sent
-		expect(mockPostMessage).toHaveBeenCalledWith({
-			type: "requestHuggingFaceModels",
-		})
-	})
-
-	it("should display loading state while fetching models", () => {
-		const apiConfiguration: Partial<ProviderSettings> = {
-			huggingFaceApiKey: "test-api-key",
-			huggingFaceModelId: "",
-		}
-
-		render(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Check for loading text in the label
-		expect(screen.getByText("settings:providers.huggingFaceLoading")).toBeInTheDocument()
-	})
-
-	it("should display model capabilities when a model is selected", async () => {
-		const apiConfiguration: Partial<ProviderSettings> = {
-			huggingFaceApiKey: "test-api-key",
-			huggingFaceModelId: "test-model",
-			huggingFaceInferenceProvider: "test-provider", // Select a specific provider to show pricing
-		}
-
-		const { rerender } = render(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Simulate receiving models from the backend
-		const mockModels = [
-			{
-				id: "test-model",
-				object: "model",
-				created: Date.now(),
-				owned_by: "test",
-				providers: [
-					{
-						provider: "test-provider",
-						status: "live" as const,
-						supports_tools: false,
-						supports_structured_output: false,
-						context_length: 8192,
-						pricing: {
-							input: 0.001,
-							output: 0.002,
-						},
-					},
-				],
-			},
-		]
-
-		// Simulate message event
-		const messageEvent = new MessageEvent("message", {
-			data: {
-				type: "huggingFaceModels",
-				huggingFaceModels: mockModels,
-			},
-		})
-		window.dispatchEvent(messageEvent)
-
-		// Re-render to trigger effect
-		rerender(
-			<HuggingFace
-				apiConfiguration={apiConfiguration as ProviderSettings}
-				setApiConfigurationField={mockSetApiConfigurationField}
-			/>,
-		)
-
-		// Check that model capabilities are displayed
-		expect(screen.getByText("Does not support images")).toBeInTheDocument()
-		expect(screen.getByText("8,192 tokens")).toBeInTheDocument()
-		// Check that both input and output prices are displayed
-		const priceElements = screen.getAllByText("$0.00 / 1M tokens")
-		expect(priceElements).toHaveLength(2) // One for input, one for output
-	})
-})

+ 0 - 9
webview-ui/src/components/settings/providers/index.ts

@@ -1,13 +1,7 @@
 export { Anthropic } from "./Anthropic"
 export { Bedrock } from "./Bedrock"
-export { Cerebras } from "./Cerebras"
-export { Chutes } from "./Chutes"
 export { DeepSeek } from "./DeepSeek"
-export { Doubao } from "./Doubao"
 export { Gemini } from "./Gemini"
-export { Groq } from "./Groq"
-export { HuggingFace } from "./HuggingFace"
-export { IOIntelligence } from "./IOIntelligence"
 export { LMStudio } from "./LMStudio"
 export { Mistral } from "./Mistral"
 export { Moonshot } from "./Moonshot"
@@ -20,15 +14,12 @@ export { QwenCode } from "./QwenCode"
 export { Roo } from "./Roo"
 export { Requesty } from "./Requesty"
 export { SambaNova } from "./SambaNova"
-export { Unbound } from "./Unbound"
 export { Vertex } from "./Vertex"
 export { VSCodeLM } from "./VSCodeLM"
 export { XAI } from "./XAI"
 export { ZAi } from "./ZAi"
 export { LiteLLM } from "./LiteLLM"
 export { Fireworks } from "./Fireworks"
-export { Featherless } from "./Featherless"
 export { VercelAiGateway } from "./VercelAiGateway"
-export { DeepInfra } from "./DeepInfra"
 export { MiniMax } from "./MiniMax"
 export { Baseten } from "./Baseten"

+ 0 - 17
webview-ui/src/components/settings/utils/providerModelConfig.ts

@@ -2,9 +2,7 @@ import type { ProviderName, ModelInfo, ProviderSettings } from "@roo-code/types"
 import {
 	anthropicDefaultModelId,
 	bedrockDefaultModelId,
-	cerebrasDefaultModelId,
 	deepSeekDefaultModelId,
-	doubaoDefaultModelId,
 	moonshotDefaultModelId,
 	geminiDefaultModelId,
 	mistralDefaultModelId,
@@ -12,12 +10,10 @@ import {
 	qwenCodeDefaultModelId,
 	vertexDefaultModelId,
 	xaiDefaultModelId,
-	groqDefaultModelId,
 	sambaNovaDefaultModelId,
 	internationalZAiDefaultModelId,
 	mainlandZAiDefaultModelId,
 	fireworksDefaultModelId,
-	featherlessDefaultModelId,
 	minimaxDefaultModelId,
 	basetenDefaultModelId,
 } from "@roo-code/types"
@@ -32,9 +28,7 @@ export interface ProviderServiceConfig {
 export const PROVIDER_SERVICE_CONFIG: Partial<Record<ProviderName, ProviderServiceConfig>> = {
 	anthropic: { serviceName: "Anthropic", serviceUrl: "https://console.anthropic.com" },
 	bedrock: { serviceName: "Amazon Bedrock", serviceUrl: "https://aws.amazon.com/bedrock" },
-	cerebras: { serviceName: "Cerebras", serviceUrl: "https://cerebras.ai" },
 	deepseek: { serviceName: "DeepSeek", serviceUrl: "https://platform.deepseek.com" },
-	doubao: { serviceName: "Doubao", serviceUrl: "https://www.volcengine.com/product/doubao" },
 	moonshot: { serviceName: "Moonshot", serviceUrl: "https://platform.moonshot.cn" },
 	gemini: { serviceName: "Google Gemini", serviceUrl: "https://ai.google.dev" },
 	mistral: { serviceName: "Mistral", serviceUrl: "https://console.mistral.ai" },
@@ -42,11 +36,9 @@ export const PROVIDER_SERVICE_CONFIG: Partial<Record<ProviderName, ProviderServi
 	"qwen-code": { serviceName: "Qwen Code", serviceUrl: "https://dashscope.console.aliyun.com" },
 	vertex: { serviceName: "GCP Vertex AI", serviceUrl: "https://console.cloud.google.com/vertex-ai" },
 	xai: { serviceName: "xAI", serviceUrl: "https://x.ai" },
-	groq: { serviceName: "Groq", serviceUrl: "https://console.groq.com" },
 	sambanova: { serviceName: "SambaNova", serviceUrl: "https://sambanova.ai" },
 	zai: { serviceName: "Z.ai", serviceUrl: "https://z.ai" },
 	fireworks: { serviceName: "Fireworks AI", serviceUrl: "https://fireworks.ai" },
-	featherless: { serviceName: "Featherless AI", serviceUrl: "https://featherless.ai" },
 	minimax: { serviceName: "MiniMax", serviceUrl: "https://minimax.chat" },
 	baseten: { serviceName: "Baseten", serviceUrl: "https://baseten.co" },
 	ollama: { serviceName: "Ollama", serviceUrl: "https://ollama.ai" },
@@ -60,9 +52,7 @@ export const PROVIDER_SERVICE_CONFIG: Partial<Record<ProviderName, ProviderServi
 export const PROVIDER_DEFAULT_MODEL_IDS: Partial<Record<ProviderName, string>> = {
 	anthropic: anthropicDefaultModelId,
 	bedrock: bedrockDefaultModelId,
-	cerebras: cerebrasDefaultModelId,
 	deepseek: deepSeekDefaultModelId,
-	doubao: doubaoDefaultModelId,
 	moonshot: moonshotDefaultModelId,
 	gemini: geminiDefaultModelId,
 	mistral: mistralDefaultModelId,
@@ -70,11 +60,9 @@ export const PROVIDER_DEFAULT_MODEL_IDS: Partial<Record<ProviderName, string>> =
 	"qwen-code": qwenCodeDefaultModelId,
 	vertex: vertexDefaultModelId,
 	xai: xaiDefaultModelId,
-	groq: groqDefaultModelId,
 	sambanova: sambaNovaDefaultModelId,
 	zai: internationalZAiDefaultModelId,
 	fireworks: fireworksDefaultModelId,
-	featherless: featherlessDefaultModelId,
 	minimax: minimaxDefaultModelId,
 	baseten: basetenDefaultModelId,
 }
@@ -130,19 +118,14 @@ export const isStaticModelProvider = (provider: ProviderName): boolean => {
 export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [
 	"openrouter",
 	"requesty",
-	"unbound",
-	"deepinfra",
 	"openai", // OpenAI Compatible
 	"openai-codex", // OpenAI Codex has custom UI with auth and rate limits
 	"litellm",
-	"io-intelligence",
 	"vercel-ai-gateway",
 	"roo",
-	"chutes",
 	"ollama",
 	"lmstudio",
 	"vscode-lm",
-	"huggingface",
 ]
 
 /**

+ 1 - 21
webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts

@@ -61,9 +61,7 @@ describe("useSelectedModel", () => {
 						"test-model": baseModelInfo,
 					},
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -124,9 +122,7 @@ describe("useSelectedModel", () => {
 						},
 					},
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -191,9 +187,7 @@ describe("useSelectedModel", () => {
 						"test-model": baseModelInfo,
 					},
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -245,9 +239,7 @@ describe("useSelectedModel", () => {
 				data: {
 					openrouter: { "test-model": baseModelInfo },
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -288,9 +280,7 @@ describe("useSelectedModel", () => {
 						},
 					},
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -350,7 +340,7 @@ describe("useSelectedModel", () => {
 
 		it("should NOT set loading when openrouter provider metadata is loading but provider is static (anthropic)", () => {
 			mockUseRouterModels.mockReturnValue({
-				data: { openrouter: {}, requesty: {}, unbound: {}, litellm: {}, "io-intelligence": {} },
+				data: { openrouter: {}, requesty: {}, litellm: {} },
 				isLoading: false,
 				isError: false,
 			} as any)
@@ -418,9 +408,7 @@ describe("useSelectedModel", () => {
 				data: {
 					openrouter: {},
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -490,9 +478,7 @@ describe("useSelectedModel", () => {
 				data: {
 					openrouter: {},
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -518,7 +504,6 @@ describe("useSelectedModel", () => {
 				data: {
 					openrouter: {},
 					requesty: {},
-					unbound: {},
 					litellm: {
 						"existing-model": {
 							maxTokens: 4096,
@@ -527,7 +512,6 @@ describe("useSelectedModel", () => {
 							supportsPromptCache: false,
 						},
 					},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -561,11 +545,9 @@ describe("useSelectedModel", () => {
 				data: {
 					openrouter: {},
 					requesty: {},
-					unbound: {},
 					litellm: {
 						"custom-model": customModelInfo,
 					},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,
@@ -591,9 +573,7 @@ describe("useSelectedModel", () => {
 				data: {
 					openrouter: {},
 					requesty: {},
-					unbound: {},
 					litellm: {},
-					"io-intelligence": {},
 				},
 				isLoading: false,
 				isError: false,

+ 19 - 75
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -6,7 +6,6 @@ import {
 	type RouterModels,
 	anthropicModels,
 	bedrockModels,
-	cerebrasModels,
 	deepSeekModels,
 	moonshotModels,
 	minimaxModels,
@@ -16,17 +15,13 @@ import {
 	openAiNativeModels,
 	vertexModels,
 	xaiModels,
-	groqModels,
 	vscodeLlmModels,
 	vscodeLlmDefaultModelId,
 	openAiCodexModels,
 	sambaNovaModels,
-	doubaoModels,
 	internationalZAiModels,
 	mainlandZAiModels,
 	fireworksModels,
-	featherlessModels,
-	ioIntelligenceModels,
 	basetenModels,
 	qwenCodeModels,
 	litellmDefaultModelInfo,
@@ -34,6 +29,7 @@ import {
 	BEDROCK_1M_CONTEXT_MODEL_IDS,
 	VERTEX_1M_CONTEXT_MODEL_IDS,
 	isDynamicProvider,
+	isRetiredProvider,
 	getProviderDefaultModelId,
 } from "@roo-code/types"
 
@@ -56,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,
 	})
 
@@ -73,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") &&
@@ -91,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,
@@ -160,11 +159,6 @@ function getSelectedModel({
 			const routerInfo = routerModels.requesty?.[id]
 			return { id, info: routerInfo }
 		}
-		case "unbound": {
-			const id = getValidatedModelId(apiConfiguration.unboundModelId, routerModels.unbound, defaultModelId)
-			const routerInfo = routerModels.unbound?.[id]
-			return { id, info: routerInfo }
-		}
 		case "litellm": {
 			const id = getValidatedModelId(apiConfiguration.litellmModelId, routerModels.litellm, defaultModelId)
 			const routerInfo = routerModels.litellm?.[id]
@@ -175,26 +169,6 @@ function getSelectedModel({
 			const info = xaiModels[id as keyof typeof xaiModels]
 			return info ? { id, info } : { id, info: undefined }
 		}
-		case "groq": {
-			const id = apiConfiguration.apiModelId ?? defaultModelId
-			const info = groqModels[id as keyof typeof groqModels]
-			return { id, info }
-		}
-		case "huggingface": {
-			const id = apiConfiguration.huggingFaceModelId ?? "meta-llama/Llama-3.3-70B-Instruct"
-			const info = {
-				maxTokens: 8192,
-				contextWindow: 131072,
-				supportsImages: false,
-				supportsPromptCache: false,
-			}
-			return { id, info }
-		}
-		case "chutes": {
-			const id = getValidatedModelId(apiConfiguration.apiModelId, routerModels.chutes, defaultModelId)
-			const info = routerModels.chutes?.[id]
-			return { id, info }
-		}
 		case "baseten": {
 			const id = apiConfiguration.apiModelId ?? defaultModelId
 			const info = basetenModels[id as keyof typeof basetenModels]
@@ -257,11 +231,6 @@ function getSelectedModel({
 			const info = deepSeekModels[id as keyof typeof deepSeekModels]
 			return { id, info }
 		}
-		case "doubao": {
-			const id = apiConfiguration.apiModelId ?? defaultModelId
-			const info = doubaoModels[id as keyof typeof doubaoModels]
-			return { id, info }
-		}
 		case "moonshot": {
 			const id = apiConfiguration.apiModelId ?? defaultModelId
 			const info = moonshotModels[id as keyof typeof moonshotModels]
@@ -320,11 +289,6 @@ function getSelectedModel({
 				info: modelInfo ? { ...lMStudioDefaultModelInfo, ...modelInfo } : undefined,
 			}
 		}
-		case "deepinfra": {
-			const id = getValidatedModelId(apiConfiguration.deepInfraModelId, routerModels.deepinfra, defaultModelId)
-			const info = routerModels.deepinfra?.[id]
-			return { id, info }
-		}
 		case "vscode-lm": {
 			const id = apiConfiguration?.vsCodeLmModelSelector
 				? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
@@ -333,11 +297,6 @@ function getSelectedModel({
 			const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels]
 			return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images.
 		}
-		case "cerebras": {
-			const id = apiConfiguration.apiModelId ?? defaultModelId
-			const info = cerebrasModels[id as keyof typeof cerebrasModels]
-			return { id, info }
-		}
 		case "sambanova": {
 			const id = apiConfiguration.apiModelId ?? defaultModelId
 			const info = sambaNovaModels[id as keyof typeof sambaNovaModels]
@@ -348,21 +307,6 @@ function getSelectedModel({
 			const info = fireworksModels[id as keyof typeof fireworksModels]
 			return { id, info }
 		}
-		case "featherless": {
-			const id = apiConfiguration.apiModelId ?? defaultModelId
-			const info = featherlessModels[id as keyof typeof featherlessModels]
-			return { id, info }
-		}
-		case "io-intelligence": {
-			const id = getValidatedModelId(
-				apiConfiguration.ioIntelligenceModelId,
-				routerModels["io-intelligence"],
-				defaultModelId,
-			)
-			const info =
-				routerModels["io-intelligence"]?.[id] ?? ioIntelligenceModels[id as keyof typeof ioIntelligenceModels]
-			return { id, info }
-		}
 		case "roo": {
 			const id = getValidatedModelId(apiConfiguration.apiModelId, routerModels.roo, defaultModelId)
 			const info = routerModels.roo?.[id]

+ 5 - 0
webview-ui/src/i18n/locales/ca/chat.json

@@ -531,5 +531,10 @@
 	},
 	"readCommandOutput": {
 		"title": "Roo read command output"
+	},
+	"retiredProvider": {
+		"title": "Proveïdor ja no compatible",
+		"message": "Ho sentim, aquest proveïdor ja no és compatible. Hem vist molt pocs usuaris de Roo que realment l'utilitzaven i necessitem reduir l'abast del nostre codi per poder seguir avançant ràpidament i servir bé la nostra comunitat en aquest espai. Va ser una decisió molt difícil, però ens permet centrar-nos en el que més t'importa. Ho sabem, és una llàstima.",
+		"openSettings": "Obrir configuració"
 	}
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов