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 dd593e1056.

* 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
This commit is contained in:
Hannes Rudolph 2026-02-09 09:40:59 -07:00 committed by GitHub
parent 7afa43635f
commit ef2fec9a23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
138 changed files with 892 additions and 9972 deletions

View file

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

View file

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

View file

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

View file

@ -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: [] },

View file

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

View file

@ -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:
"DeepSeekV3.1Terminus is an update to V3.1 that improves language consistency by reducing CN/EN mixups 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 longcontext training and inference efficiency while maintaining performance comparable to V3.1Terminus.",
},
"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:
"Qwen3VL235BA22BThinking is an openweight MoE visionlanguage model (235B total, ~22B activated) optimized for deliberate multistep reasoning with strong textimagevideo understanding and longcontext capabilities.",
},
} as const satisfies Record<string, ModelInfo>
export const chutesDefaultModelInfo: ModelInfo = chutesModels[chutesDefaultModelId]

View file

@ -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.",
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

35
pnpm-lock.yaml generated
View file

@ -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(react@18.3.1)
@ -752,9 +752,6 @@ importers:
'@ai-sdk/baseten':
specifier: ^1.0.31
version: 1.0.31(zod@3.25.76)
'@ai-sdk/cerebras':
specifier: ^2.0.31
version: 2.0.31(zod@3.25.76)
'@ai-sdk/deepseek':
specifier: ^2.0.18
version: 2.0.18(zod@3.25.76)
@ -767,9 +764,6 @@ importers:
'@ai-sdk/google-vertex':
specifier: ^4.0.45
version: 4.0.45(zod@3.25.76)
'@ai-sdk/groq':
specifier: ^3.0.22
version: 3.0.22(zod@3.25.76)
'@ai-sdk/mistral':
specifier: ^3.0.19
version: 3.0.19(zod@3.25.76)
@ -1441,12 +1435,6 @@ packages:
peerDependencies:
zod: 3.25.76
'@ai-sdk/cerebras@2.0.31':
resolution: {integrity: sha512-s7o4BRsbG2RFina4VwHs46RWlQPGCL1CrfOoMomYneJeA0CgpxPigPqwlrupaWWB42KIDDHN5gNOIsLst0oOPg==}
engines: {node: '>=18'}
peerDependencies:
zod: 3.25.76
'@ai-sdk/deepseek@2.0.18':
resolution: {integrity: sha512-AwtmFm7acnCsz3z82Yu5QKklSZz+cBwtxrc2hbw47tPF/38xr1zX3Vf/pP627EHwWkLV18UWivIxg0SHPP2w3A==}
engines: {node: '>=18'}
@ -1477,12 +1465,6 @@ packages:
peerDependencies:
zod: 3.25.76
'@ai-sdk/groq@3.0.22':
resolution: {integrity: sha512-QBkqBmlts2qz2vX54gXeP9IdztMFxZw7xPNwjOjHYhEL7RynzB2aFafPIbAYTVNosrU0YEETxhw9LISjS2TtXw==}
engines: {node: '>=18'}
peerDependencies:
zod: 3.25.76
'@ai-sdk/mistral@3.0.19':
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/cerebras@2.0.31(zod@3.25.76)':
dependencies:
'@ai-sdk/openai-compatible': 2.0.28(zod@3.25.76)
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.14(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/deepseek@2.0.18(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.8
@ -11111,12 +11086,6 @@ snapshots:
'@ai-sdk/provider-utils': 4.0.14(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/groq@3.0.22(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.14(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/mistral@3.0.19(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.8

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()
})
})

View file

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

View file

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

View file

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

View file

@ -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() }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import {
getModelId,
type ProviderName,
isProviderName,
isRetiredProvider,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
@ -359,8 +360,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

View file

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

View file

@ -566,6 +566,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

View file

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

View file

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

View file

@ -41,6 +41,7 @@ import {
TodoItem,
getApiProtocol,
getModelId,
isRetiredProvider,
isIdleAsk,
isInteractiveAsk,
isResumableAsk,
@ -1035,7 +1036,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Other providers (notably Gemini 3) use different signature semantics (e.g. `thoughtSignature`)
// and require round-tripping the signature in their own format.
const modelId = getModelId(this.apiConfiguration)
const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId)
const apiProvider = this.apiConfiguration.apiProvider
const apiProtocol = getApiProtocol(
apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId,
)
const isAnthropicProtocol = apiProtocol === "anthropic"
// Start from the original assistant message
@ -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 =

View file

@ -45,6 +45,7 @@ import {
DEFAULT_MODES,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
getModelId,
isRetiredProvider,
} from "@roo-code/types"
import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts"
import { TelemetryService } from "@roo-code/telemetry"
@ -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,

View file

@ -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,
})

View file

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

View file

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

View file

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

View file

@ -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": {

View file

@ -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": {

View file

@ -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."
},

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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ıı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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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} />
)}
{selectedProvider === "gemini" && (
<Gemini apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "openai" && (
<OpenAICompatible
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "lmstudio" && (
<LMStudio apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "deepseek" && (
<DeepSeek
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "doubao" && (
<Doubao
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "qwen-code" && (
<QwenCode
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "moonshot" && (
<Moonshot
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "minimax" && (
<MiniMax apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "vscode-lm" && (
<VSCodeLM apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "ollama" && (
<Ollama apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "xai" && (
<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "groq" && (
<Groq apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "huggingface" && (
<HuggingFace apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "cerebras" && (
<Cerebras apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "chutes" && (
<Chutes
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "litellm" && (
<LiteLLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "sambanova" && (
<SambaNova apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "zai" && (
<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "io-intelligence" && (
<IOIntelligence
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 === "fireworks" && (
<Fireworks apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "roo" && (
<Roo
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
cloudIsAuthenticated={cloudIsAuthenticated}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "featherless" && (
<Featherless apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{/* Generic model picker for providers with static models */}
{shouldUseGenericModelPicker(selectedProvider) && (
{isRetiredSelectedProvider ? (
<div
className="rounded-md border border-vscode-panel-border px-3 py-2 text-sm text-vscode-descriptionForeground"
data-testid="retired-provider-message">
{t("settings:providers.retiredProviderMessage")}
</div>
) : (
<>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={getDefaultModelIdForProvider(selectedProvider, apiConfiguration)}
models={getStaticModelsForProvider(selectedProvider, t("settings:labels.useCustomArn"))}
modelIdKey="apiModelId"
serviceName={getProviderServiceConfig(selectedProvider).serviceName}
serviceUrl={getProviderServiceConfig(selectedProvider).serviceUrl}
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
simplifySettings={fromWelcomeView}
onModelChange={(modelId) =>
handleModelChangeSideEffects(selectedProvider, modelId, setApiConfigurationField)
}
/>
{selectedProvider === "openrouter" && (
<OpenRouter
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
selectedModelId={selectedModelId}
uriScheme={uriScheme}
simplifySettings={fromWelcomeView}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
/>
)}
{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
<BedrockCustomArn
{selectedProvider === "requesty" && (
<Requesty
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
refetchRouterModels={refetchRouterModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "anthropic" && (
<Anthropic
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "openai-codex" && (
<OpenAICodex
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
openAiCodexIsAuthenticated={openAiCodexIsAuthenticated}
/>
)}
{selectedProvider === "openai-native" && (
<OpenAI
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "mistral" && (
<Mistral
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "baseten" && (
<Baseten
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "bedrock" && (
<Bedrock
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "vertex" && (
<Vertex
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
</>
)}
{!fromWelcomeView && (
<ThinkingBudget
key={`${selectedProvider}-${selectedModelId}`}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{/* Gate Verbosity UI by capability flag */}
{!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
<Verbosity
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{!fromWelcomeView && (
<Collapsible open={isAdvancedSettingsOpen} onOpenChange={setIsAdvancedSettingsOpen}>
<CollapsibleTrigger className="flex items-center gap-1 w-full cursor-pointer hover:opacity-80 mb-2">
<span className={`codicon codicon-chevron-${isAdvancedSettingsOpen ? "down" : "right"}`}></span>
<span className="font-medium">{t("settings:advancedSettings.title")}</span>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3">
<TodoListSettingsControl
todoListEnabled={apiConfiguration.todoListEnabled}
onChange={(field, value) => setApiConfigurationField(field, value)}
{selectedProvider === "gemini" && (
<Gemini
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
{selectedModelInfo?.supportsTemperature !== false && (
<TemperatureControl
value={apiConfiguration.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}
maxValue={2}
defaultValue={selectedModelInfo?.defaultTemperature}
)}
{selectedProvider === "openai" && (
<OpenAICompatible
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "lmstudio" && (
<LMStudio
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "deepseek" && (
<DeepSeek
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "qwen-code" && (
<QwenCode
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "moonshot" && (
<Moonshot
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "minimax" && (
<MiniMax
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "vscode-lm" && (
<VSCodeLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "ollama" && (
<Ollama
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "xai" && (
<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "litellm" && (
<LiteLLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "sambanova" && (
<SambaNova
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "zai" && (
<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "vercel-ai-gateway" && (
<VercelAiGateway
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "fireworks" && (
<Fireworks
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "roo" && (
<Roo
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
cloudIsAuthenticated={cloudIsAuthenticated}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{/* Generic model picker for providers with static models */}
{activeSelectedProvider && shouldUseGenericModelPicker(activeSelectedProvider) && (
<>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={getDefaultModelIdForProvider(activeSelectedProvider, apiConfiguration)}
models={getStaticModelsForProvider(
activeSelectedProvider,
t("settings:labels.useCustomArn"),
)}
modelIdKey="apiModelId"
serviceName={getProviderServiceConfig(activeSelectedProvider).serviceName}
serviceUrl={getProviderServiceConfig(activeSelectedProvider).serviceUrl}
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
simplifySettings={fromWelcomeView}
onModelChange={(modelId) =>
handleModelChangeSideEffects(
activeSelectedProvider,
modelId,
setApiConfigurationField,
)
}
/>
)}
<RateLimitSecondsControl
value={apiConfiguration.rateLimitSeconds || 0}
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
/>
<ConsecutiveMistakeLimitControl
value={
apiConfiguration.consecutiveMistakeLimit !== undefined
? apiConfiguration.consecutiveMistakeLimit
: DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
}
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
/>
{selectedProvider === "openrouter" &&
openRouterModelProviders &&
Object.keys(openRouterModelProviders).length > 0 && (
<div>
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.openRouter.providerRouting.title")}
</label>
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
<Select
value={
apiConfiguration?.openRouterSpecificProvider ||
OPENROUTER_DEFAULT_PROVIDER_NAME
}
onValueChange={(value) =>
setApiConfigurationField("openRouterSpecificProvider", value)
}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
{OPENROUTER_DEFAULT_PROVIDER_NAME}
</SelectItem>
{Object.entries(openRouterModelProviders).map(([value, { label }]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.openRouter.providerRouting.description")}{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">
{t("settings:providers.openRouter.providerRouting.learnMore")}.
</a>
</div>
</div>
{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
<BedrockCustomArn
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
</CollapsibleContent>
</Collapsible>
</>
)}
{!fromWelcomeView && (
<ThinkingBudget
key={`${selectedProvider}-${selectedModelId}`}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{/* Gate Verbosity UI by capability flag */}
{!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
<Verbosity
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{!fromWelcomeView && (
<Collapsible open={isAdvancedSettingsOpen} onOpenChange={setIsAdvancedSettingsOpen}>
<CollapsibleTrigger className="flex items-center gap-1 w-full cursor-pointer hover:opacity-80 mb-2">
<span
className={`codicon codicon-chevron-${isAdvancedSettingsOpen ? "down" : "right"}`}></span>
<span className="font-medium">{t("settings:advancedSettings.title")}</span>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3">
<TodoListSettingsControl
todoListEnabled={apiConfiguration.todoListEnabled}
onChange={(field, value) => setApiConfigurationField(field, value)}
/>
{selectedModelInfo?.supportsTemperature !== false && (
<TemperatureControl
value={apiConfiguration.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}
maxValue={2}
defaultValue={selectedModelInfo?.defaultTemperature}
/>
)}
<RateLimitSecondsControl
value={apiConfiguration.rateLimitSeconds || 0}
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
/>
<ConsecutiveMistakeLimitControl
value={
apiConfiguration.consecutiveMistakeLimit !== undefined
? apiConfiguration.consecutiveMistakeLimit
: DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
}
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
/>
{selectedProvider === "openrouter" &&
openRouterModelProviders &&
Object.keys(openRouterModelProviders).length > 0 && (
<div>
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.openRouter.providerRouting.title")}
</label>
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
<Select
value={
apiConfiguration?.openRouterSpecificProvider ||
OPENROUTER_DEFAULT_PROVIDER_NAME
}
onValueChange={(value) =>
setApiConfigurationField("openRouterSpecificProvider", value)
}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
{OPENROUTER_DEFAULT_PROVIDER_NAME}
</SelectItem>
{Object.entries(openRouterModelProviders).map(
([value, { label }]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
),
)}
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.openRouter.providerRouting.description")}{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">
{t("settings:providers.openRouter.providerRouting.learnMore")}.
</a>
</div>
</div>
)}
</CollapsibleContent>
</Collapsible>
)}
</>
)}
</div>
)

View file

@ -3,7 +3,7 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Trans } from "react-i18next"
import { ChevronsUpDown, Check, X, Info } from "lucide-react"
import type { ProviderSettings, ModelInfo, OrganizationAllowList } from "@roo-code/types"
import { type ProviderSettings, type ModelInfo, type OrganizationAllowList, isRetiredProvider } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
@ -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("")

View file

@ -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", () => {

View file

@ -662,4 +662,31 @@ describe("ApiOptions", () => {
useExtensionStateMock.mockRestore()
})
})
it("renders retired provider message and hides provider-specific forms", () => {
renderApiOptions({
apiConfiguration: {
apiProvider: "groq",
},
})
expect(screen.getByTestId("retired-provider-message")).toHaveTextContent(
"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")
})
})

View file

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

View file

@ -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>
)}
</>
)
}

View file

@ -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}
/>
</>
)
}

View file

@ -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}
/>
</>
)
}

View file

@ -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>
)}
</>
)
}

View file

@ -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>
)}
</>
)
}

View file

@ -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>
)}
</>
)
}

View file

@ -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>
)}
</>
)
}

View file

@ -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}
/>
</>
)
}

View file

@ -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}
/>
</>
)
}

View file

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

View file

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

View file

@ -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",
]
/**

View file

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

View file

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

View file

@ -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ó"
}
}

Some files were not shown because too many files have changed in this diff Show more