feat: Add Groq and Chutes API providers (#3034)

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Chris Estreich <cestreich@gmail.com>
This commit is contained in:
Shariq Riaz 2025-05-06 02:16:27 +04:00 • committed by GitHub
parent 6666d43c7d
commit 883be32b1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 935 additions and 0 deletions

View file

@ -23,6 +23,8 @@ import { RequestyHandler } from "./providers/requesty"
import { HumanRelayHandler } from "./providers/human-relay"
import { FakeAIHandler } from "./providers/fake-ai"
import { XAIHandler } from "./providers/xai"
import { GroqHandler } from "./providers/groq"
import { ChutesHandler } from "./providers/chutes"
export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
@ -88,6 +90,10 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new FakeAIHandler(options)
case "xai":
return new XAIHandler(options)
case "groq":
return new GroqHandler(options)
case "chutes":
return new ChutesHandler(options)
default:
return new AnthropicHandler(options)
}

View file

@ -0,0 +1,142 @@
// npx jest src/api/providers/__tests__/chutes.test.ts
import OpenAI from "openai"
import { Anthropic } from "@anthropic-ai/sdk"
import { ChutesModelId, chutesDefaultModelId, chutesModels } from "../../../shared/api"
import { ChutesHandler } from "../chutes"
jest.mock("openai", () => {
const createMock = jest.fn()
return jest.fn(() => ({ chat: { completions: { create: createMock } } }))
})
describe("ChutesHandler", () => {
let handler: ChutesHandler
let mockCreate: jest.Mock
beforeEach(() => {
jest.clearAllMocks()
mockCreate = (OpenAI as unknown as jest.Mock)().chat.completions.create
handler = new ChutesHandler({})
})
test("should use the correct Chutes base URL", () => {
new ChutesHandler({})
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://llm.chutes.ai/v1" }))
})
test("should use the provided API key", () => {
const chutesApiKey = "test-chutes-api-key"
new ChutesHandler({ chutesApiKey })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: chutesApiKey }))
})
test("should return default model when no model is specified", () => {
const model = handler.getModel()
expect(model.id).toBe(chutesDefaultModelId)
expect(model.info).toEqual(chutesModels[chutesDefaultModelId])
})
test("should return specified model when valid model is provided", () => {
const testModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
const handlerWithModel = new ChutesHandler({ apiModelId: testModelId })
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(chutesModels[testModelId])
})
test("completePrompt method should return text from Chutes API", async () => {
const expectedResponse = "This is a test response from Chutes"
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
const result = await handler.completePrompt("test prompt")
expect(result).toBe(expectedResponse)
})
test("should handle errors in completePrompt", async () => {
const errorMessage = "Chutes API error"
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`Chutes completion error: ${errorMessage}`)
})
test("createMessage should yield text content from stream", async () => {
const testContent = "This is test content from Chutes stream"
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: jest
.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 })
})
test("createMessage should yield usage data from stream", async () => {
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: jest
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },
})
.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 })
})
test("createMessage should pass correct parameters to Chutes client", async () => {
const modelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
const modelInfo = chutesModels[modelId]
const handlerWithModel = new ChutesHandler({ apiModelId: modelId })
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const systemPrompt = "Test system prompt for Chutes"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Chutes" }]
const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.5,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
}),
)
})
})

View file

@ -0,0 +1,142 @@
// npx jest src/api/providers/__tests__/groq.test.ts
import OpenAI from "openai"
import { Anthropic } from "@anthropic-ai/sdk"
import { GroqModelId, groqDefaultModelId, groqModels } from "../../../shared/api"
import { GroqHandler } from "../groq"
jest.mock("openai", () => {
const createMock = jest.fn()
return jest.fn(() => ({ chat: { completions: { create: createMock } } }))
})
describe("GroqHandler", () => {
let handler: GroqHandler
let mockCreate: jest.Mock
beforeEach(() => {
jest.clearAllMocks()
mockCreate = (OpenAI as unknown as jest.Mock)().chat.completions.create
handler = new GroqHandler({})
})
test("should use the correct Groq base URL", () => {
new GroqHandler({})
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.groq.com/openai/v1" }))
})
test("should use the provided API key", () => {
const groqApiKey = "test-groq-api-key"
new GroqHandler({ groqApiKey })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: groqApiKey }))
})
test("should return default model when no model is specified", () => {
const model = handler.getModel()
expect(model.id).toBe(groqDefaultModelId) // Use groqDefaultModelId
expect(model.info).toEqual(groqModels[groqDefaultModelId]) // Use groqModels
})
test("should return specified model when valid model is provided", () => {
const testModelId: GroqModelId = "llama-3.3-70b-versatile" // Use a valid Groq model ID and type
const handlerWithModel = new GroqHandler({ apiModelId: testModelId }) // Instantiate GroqHandler
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(groqModels[testModelId]) // Use groqModels
})
test("completePrompt method should return text from Groq API", async () => {
const expectedResponse = "This is a test response from Groq"
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
const result = await handler.completePrompt("test prompt")
expect(result).toBe(expectedResponse)
})
test("should handle errors in completePrompt", async () => {
const errorMessage = "Groq API error"
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`Groq completion error: ${errorMessage}`)
})
test("createMessage should yield text content from stream", async () => {
const testContent = "This is test content from Groq stream"
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: jest
.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 })
})
test("createMessage should yield usage data from stream", async () => {
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: jest
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },
})
.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 })
})
test("createMessage should pass correct parameters to Groq client", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const modelInfo = groqModels[modelId]
const handlerWithModel = new GroqHandler({ apiModelId: modelId })
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const systemPrompt = "Test system prompt for Groq"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Groq" }]
const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.5,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
}),
)
})
})

View file

@ -0,0 +1,129 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { SingleCompletionHandler } from "../index"
import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
type BaseOpenAiCompatibleProviderOptions<ModelName extends string> = ApiHandlerOptions & {
providerName: string
baseURL: string
defaultProviderModelId: ModelName
providerModels: Record<ModelName, ModelInfo>
defaultTemperature?: number
}
export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
extends BaseProvider
implements SingleCompletionHandler
{
protected readonly providerName: string
protected readonly baseURL: string
protected readonly defaultTemperature: number
protected readonly defaultProviderModelId: ModelName
protected readonly providerModels: Record<ModelName, ModelInfo>
protected readonly options: ApiHandlerOptions
private client: OpenAI
constructor({
providerName,
baseURL,
defaultProviderModelId,
providerModels,
defaultTemperature,
...options
}: BaseOpenAiCompatibleProviderOptions<ModelName>) {
super()
this.providerName = providerName
this.baseURL = baseURL
this.defaultProviderModelId = defaultProviderModelId
this.providerModels = providerModels
this.defaultTemperature = defaultTemperature ?? 0
this.options = options
if (!this.options.apiKey) {
throw new Error("API key is required")
}
this.client = new OpenAI({
baseURL,
apiKey: this.options.apiKey,
defaultHeaders: DEFAULT_HEADERS,
})
}
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const {
id: model,
info: { maxTokens: max_tokens },
} = this.getModel()
const temperature = this.options.modelTemperature ?? this.defaultTemperature
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model,
max_tokens,
temperature,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
}
const stream = await this.client.chat.completions.create(params)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
async completePrompt(prompt: string): Promise<string> {
const { id: modelId } = this.getModel()
try {
const response = await this.client.chat.completions.create({
model: modelId,
messages: [{ role: "user", content: prompt }],
})
return response.choices[0]?.message.content || ""
} catch (error) {
if (error instanceof Error) {
throw new Error(`${this.providerName} completion error: ${error.message}`)
}
throw error
}
}
override getModel() {
const id =
this.options.apiModelId && this.options.apiModelId in this.providerModels
? (this.options.apiModelId as ModelName)
: this.defaultProviderModelId
return { id, info: this.providerModels[id] }
}
}

View file

@ -0,0 +1,17 @@
import { ApiHandlerOptions, ChutesModelId, chutesDefaultModelId, chutesModels } from "../../shared/api"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
export class ChutesHandler extends BaseOpenAiCompatibleProvider<ChutesModelId> {
constructor(options: ApiHandlerOptions) {
super({
...options,
providerName: "Chutes",
baseURL: "https://llm.chutes.ai/v1",
apiKey: options.chutesApiKey,
defaultProviderModelId: chutesDefaultModelId,
providerModels: chutesModels,
defaultTemperature: 0.5,
})
}
}

17
src/api/providers/groq.ts Normal file
View file

@ -0,0 +1,17 @@
import { ApiHandlerOptions, GroqModelId, groqDefaultModelId, groqModels } from "../../shared/api" // Updated imports for Groq
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
export class GroqHandler extends BaseOpenAiCompatibleProvider<GroqModelId> {
constructor(options: ApiHandlerOptions) {
super({
...options,
providerName: "Groq",
baseURL: "https://api.groq.com/openai/v1",
apiKey: options.groqApiKey,
defaultProviderModelId: groqDefaultModelId,
providerModels: groqModels,
defaultTemperature: 0.5,
})
}
}

View file

@ -21,6 +21,8 @@ type ProviderSettings = {
| "human-relay"
| "fake-ai"
| "xai"
| "groq"
| "chutes"
)
| undefined
apiModelId?: string | undefined
@ -120,6 +122,8 @@ type ProviderSettings = {
requestyApiKey?: string | undefined
requestyModelId?: string | undefined
xaiApiKey?: string | undefined
groqApiKey?: string | undefined
chutesApiKey?: string | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
@ -158,6 +162,8 @@ type GlobalSettings = {
| "human-relay"
| "fake-ai"
| "xai"
| "groq"
| "chutes"
)
| undefined
}[]

View file

@ -22,6 +22,8 @@ type ProviderSettings = {
| "human-relay"
| "fake-ai"
| "xai"
| "groq"
| "chutes"
)
| undefined
apiModelId?: string | undefined
@ -121,6 +123,8 @@ type ProviderSettings = {
requestyApiKey?: string | undefined
requestyModelId?: string | undefined
xaiApiKey?: string | undefined
groqApiKey?: string | undefined
chutesApiKey?: string | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
@ -161,6 +165,8 @@ type GlobalSettings = {
| "human-relay"
| "fake-ai"
| "xai"
| "groq"
| "chutes"
)
| undefined
}[]

View file

@ -89,5 +89,11 @@
"path_placeholder": "D:\\RooCodeStorage",
"enter_absolute_path": "Introdueix una ruta completa (p. ex. D:\\RooCodeStorage o /home/user/storage)",
"enter_valid_path": "Introdueix una ruta vàlida"
},
"settings": {
"providers": {
"groqApiKey": "Clau API de Groq",
"getGroqApiKey": "Obté la clau API de Groq"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Was soll Roo tun?",
"task_placeholder": "Gib deine Aufgabe hier ein"
},
"settings": {
"providers": {
"groqApiKey": "Groq API-Schlüssel",
"getGroqApiKey": "Groq API-Schlüssel erhalten"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "¿Qué debe hacer Roo?",
"task_placeholder": "Escribe tu tarea aquí"
},
"settings": {
"providers": {
"groqApiKey": "Clave API de Groq",
"getGroqApiKey": "Obtener clave API de Groq"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Que doit faire Roo ?",
"task_placeholder": "Écris ta tâche ici"
},
"settings": {
"providers": {
"groqApiKey": "Clé API Groq",
"getGroqApiKey": "Obtenir la clé API Groq"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Roo को क्या करना है?",
"task_placeholder": "अपना कार्य यहाँ लिखें"
},
"settings": {
"providers": {
"groqApiKey": "ग्रोक एपीआई कुंजी",
"getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Cosa deve fare Roo?",
"task_placeholder": "Scrivi il tuo compito qui"
},
"settings": {
"providers": {
"groqApiKey": "Chiave API Groq",
"getGroqApiKey": "Ottieni chiave API Groq"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Rooにどんなことをさせますか?",
"task_placeholder": "タスクをここに入力してください"
},
"settings": {
"providers": {
"groqApiKey": "Groq APIキー",
"getGroqApiKey": "Groq APIキーを取得"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Roo에게 무엇을 시킬까요?",
"task_placeholder": "여기에 작업을 입력하세요"
},
"settings": {
"providers": {
"groqApiKey": "Groq API 키",
"getGroqApiKey": "Groq API 키 받기"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Co ma zrobić Roo?",
"task_placeholder": "Wpisz swoje zadanie tutaj"
},
"settings": {
"providers": {
"groqApiKey": "Klucz API Groq",
"getGroqApiKey": "Uzyskaj klucz API Groq"
}
}
}

View file

@ -89,5 +89,11 @@
"path_placeholder": "D:\\RooCodeStorage",
"enter_absolute_path": "Por favor, digite um caminho absoluto (ex: D:\\RooCodeStorage ou /home/user/storage)",
"enter_valid_path": "Por favor, digite um caminho válido"
},
"settings": {
"providers": {
"groqApiKey": "Chave de API Groq",
"getGroqApiKey": "Obter chave de API Groq"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Что должен сделать Roo?",
"task_placeholder": "Введите вашу задачу здесь"
},
"settings": {
"providers": {
"groqApiKey": "Ключ API Groq",
"getGroqApiKey": "Получить ключ API Groq"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Roo ne yapsın?",
"task_placeholder": "Görevini buraya yaz"
},
"settings": {
"providers": {
"groqApiKey": "Groq API Anahtarı",
"getGroqApiKey": "Groq API Anahtarı Al"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "Bạn muốn Roo làm gì?",
"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"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "让Roo做什么?",
"task_placeholder": "在这里输入任务"
},
"settings": {
"providers": {
"groqApiKey": "Groq API 密钥",
"getGroqApiKey": "获取 Groq API 密钥"
}
}
}

View file

@ -89,5 +89,11 @@
"input": {
"task_prompt": "讓 Roo 做什麼?",
"task_placeholder": "在這裡輸入工作"
},
"settings": {
"providers": {
"groqApiKey": "Groq API 金鑰",
"getGroqApiKey": "取得 Groq API 金鑰"
}
}
}

View file

@ -29,6 +29,8 @@ export const providerNames = [
"human-relay",
"fake-ai",
"xai",
"groq",
"chutes",
] as const
export const providerNamesSchema = z.enum(providerNames)
@ -423,6 +425,10 @@ export const providerSettingsSchema = z.object({
requestyModelId: z.string().optional(),
// X.AI (Grok)
xaiApiKey: z.string().optional(),
// Groq
groqApiKey: z.string().optional(),
// Chutes AI
chutesApiKey: z.string().optional(),
// Claude 3.7 Sonnet Thinking
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
@ -529,6 +535,10 @@ const providerSettingsRecord: ProviderSettingsRecord = {
fakeAi: undefined,
// X.AI (Grok)
xaiApiKey: undefined,
// Groq
groqApiKey: undefined,
// Chutes AI
chutesApiKey: undefined,
}
export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Keys<ProviderSettings>[]
@ -721,6 +731,8 @@ export type SecretState = Pick<
| "unboundApiKey"
| "requestyApiKey"
| "xaiApiKey"
| "groqApiKey"
| "chutesApiKey"
>
type SecretStateRecord = Record<Keys<SecretState>, undefined>
@ -740,6 +752,8 @@ const secretStateRecord: SecretStateRecord = {
unboundApiKey: undefined,
requestyApiKey: undefined,
xaiApiKey: undefined,
groqApiKey: undefined,
chutesApiKey: undefined,
}
export const SECRET_STATE_KEYS = Object.keys(secretStateRecord) as Keys<SecretState>[]

View file

@ -1400,6 +1400,251 @@ export const vscodeLlmModels = {
}
>
// Groq
// 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"
| "meta-llama/llama-4-maverick-17b-128e-instruct"
| "mistral-saba-24b"
| "qwen-qwq-32b"
| "deepseek-r1-distill-llama-70b"
export const groqDefaultModelId: GroqModelId = "llama-3.3-70b-versatile" // Defaulting to Llama3 70B Versatile
export const groqModels = {
// Models based on API response: https://api.groq.com/openai/v1/models
"llama-3.1-8b-instant": {
maxTokens: 131072,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Meta Llama 3.1 8B Instant model, 128K context.",
},
"llama-3.3-70b-versatile": {
maxTokens: 32768,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
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,
outputPrice: 0,
description: "Meta Llama 4 Scout 17B Instruct model, 128K context.",
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Meta Llama 4 Maverick 17B Instruct model, 128K context.",
},
"mistral-saba-24b": {
maxTokens: 32768,
contextWindow: 32768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Mistral Saba 24B model, 32K context.",
},
"qwen-qwq-32b": {
maxTokens: 131072,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Alibaba Qwen QwQ 32B model, 128K context.",
},
"deepseek-r1-distill-llama-70b": {
maxTokens: 131072,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek R1 Distill Llama 70B model, 128K context.",
},
} as const satisfies Record<string, ModelInfo>
// Chutes AI
// https://llm.chutes.ai/v1 (OpenAI compatible)
export type ChutesModelId =
| "deepseek-ai/DeepSeek-R1"
| "deepseek-ai/DeepSeek-V3"
| "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"
| "microsoft/MAI-DS-R1-FP8"
| "tngtech/DeepSeek-R1T-Chimera"
export const chutesDefaultModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
export const chutesModels = {
"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.",
},
"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.",
},
"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.",
},
} as const satisfies Record<string, ModelInfo>
/**
* Constants
*/

View file

@ -1613,6 +1613,49 @@ const ApiOptions = ({
</>
)}
{selectedProvider === "groq" && (
<>
<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>
)}
</>
)}
{selectedProvider === "chutes" && (
<>
<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>
{/* Add a link to get Chutes API key if available */}
{/* {!apiConfiguration?.chutesApiKey && (
<VSCodeButtonLink href="LINK_TO_CHUTES_API_KEYS" appearance="secondary">
{t("settings:providers.getChutesApiKey")}
</VSCodeButtonLink>
)} */}
</>
)}
{selectedProvider === "unbound" && (
<>
<VSCodeTextField

View file

@ -9,6 +9,8 @@ import {
openAiNativeModels,
vertexModels,
xaiModels,
groqModels,
chutesModels,
} from "@roo/shared/api"
export { REASONING_MODELS, PROMPT_CACHING_MODELS } from "@roo/shared/api"
@ -24,6 +26,8 @@ export const MODELS_BY_PROVIDER: Partial<Record<ApiProvider, Record<string, Mode
"openai-native": openAiNativeModels,
vertex: vertexModels,
xai: xaiModels,
groq: groqModels,
chutes: chutesModels,
}
export const PROVIDERS = [
@ -44,6 +48,8 @@ export const PROVIDERS = [
{ value: "requesty", label: "Requesty" },
{ value: "human-relay", label: "Human Relay" },
{ value: "xai", label: "xAI" },
{ value: "groq", label: "Groq" },
{ value: "chutes", label: "Chutes AI" },
].sort((a, b) => a.label.localeCompare(b.label))
export const VERTEX_REGIONS = [

View file

@ -19,6 +19,10 @@ import {
vertexModels,
xaiDefaultModelId,
xaiModels,
groqModels,
groqDefaultModelId,
chutesModels,
chutesDefaultModelId,
vscodeLlmModels,
vscodeLlmDefaultModelId,
openRouterDefaultModelId,
@ -84,6 +88,10 @@ function getSelectedModelInfo({
return routerModels.unbound[id] ?? routerModels.unbound[unboundDefaultModelId]
case "xai":
return xaiModels[id as keyof typeof xaiModels] ?? xaiModels[xaiDefaultModelId]
case "groq":
return groqModels[id as keyof typeof groqModels] ?? groqModels[groqDefaultModelId]
case "chutes":
return chutesModels[id as keyof typeof chutesModels] ?? chutesModels[chutesDefaultModelId]
case "bedrock":
// Special case for custom ARN.
if (id === "custom-arn") {

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Clau API d'Anthropic",
"getAnthropicApiKey": "Obtenir clau API d'Anthropic",
"anthropicUseAuthToken": "Passar la clau API d'Anthropic com a capçalera d'autorització en lloc de X-Api-Key",
"chutesApiKey": "Clau API de Chutes",
"getChutesApiKey": "Obtenir clau API de Chutes",
"deepSeekApiKey": "Clau API de DeepSeek",
"getDeepSeekApiKey": "Obtenir clau API de DeepSeek",
"geminiApiKey": "Clau API de Gemini",
"getGroqApiKey": "Obtenir clau API de Groq",
"groqApiKey": "Clau API de Groq",
"getGeminiApiKey": "Obtenir clau API de Gemini",
"openAiApiKey": "Clau API d'OpenAI",
"openAiBaseUrl": "URL base",

View file

@ -123,10 +123,14 @@
"anthropicApiKey": "Anthropic API-Schlüssel",
"getAnthropicApiKey": "Anthropic API-Schlüssel erhalten",
"anthropicUseAuthToken": "Anthropic API-Schlüssel als Authorization-Header anstelle von X-Api-Key übergeben",
"chutesApiKey": "Chutes API-Schlüssel",
"getChutesApiKey": "Chutes API-Schlüssel erhalten",
"deepSeekApiKey": "DeepSeek API-Schlüssel",
"getDeepSeekApiKey": "DeepSeek API-Schlüssel erhalten",
"geminiApiKey": "Gemini API-Schlüssel",
"getGeminiApiKey": "Gemini API-Schlüssel erhalten",
"getGroqApiKey": "Groq API-Schlüssel erhalten",
"groqApiKey": "Groq API-Schlüssel",
"openAiApiKey": "OpenAI API-Schlüssel",
"openAiBaseUrl": "Basis-URL",
"getOpenAiApiKey": "OpenAI API-Schlüssel erhalten",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic API Key",
"getAnthropicApiKey": "Get Anthropic API Key",
"anthropicUseAuthToken": "Pass Anthropic API Key as Authorization header instead of X-Api-Key",
"chutesApiKey": "Chutes API Key",
"getChutesApiKey": "Get Chutes API Key",
"deepSeekApiKey": "DeepSeek API Key",
"getDeepSeekApiKey": "Get DeepSeek API Key",
"geminiApiKey": "Gemini API Key",
"getGroqApiKey": "Get Groq API Key",
"groqApiKey": "Groq API Key",
"getGeminiApiKey": "Get Gemini API Key",
"openAiApiKey": "OpenAI API Key",
"openAiBaseUrl": "Base URL",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Clave API de Anthropic",
"getAnthropicApiKey": "Obtener clave API de Anthropic",
"anthropicUseAuthToken": "Pasar la clave API de Anthropic como encabezado de autorización en lugar de X-Api-Key",
"chutesApiKey": "Clave API de Chutes",
"getChutesApiKey": "Obtener clave API de Chutes",
"deepSeekApiKey": "Clave API de DeepSeek",
"getDeepSeekApiKey": "Obtener clave API de DeepSeek",
"geminiApiKey": "Clave API de Gemini",
"getGroqApiKey": "Obtener clave API de Groq",
"groqApiKey": "Clave API de Groq",
"getGeminiApiKey": "Obtener clave API de Gemini",
"openAiApiKey": "Clave API de OpenAI",
"openAiBaseUrl": "URL base",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Clé API Anthropic",
"getAnthropicApiKey": "Obtenir la clé API Anthropic",
"anthropicUseAuthToken": "Passer la clé API Anthropic comme en-tête d'autorisation au lieu de X-Api-Key",
"chutesApiKey": "Clé API Chutes",
"getChutesApiKey": "Obtenir la clé API Chutes",
"deepSeekApiKey": "Clé API DeepSeek",
"getDeepSeekApiKey": "Obtenir la clé API DeepSeek",
"geminiApiKey": "Clé API Gemini",
"getGroqApiKey": "Obtenir la clé API Groq",
"groqApiKey": "Clé API Groq",
"getGeminiApiKey": "Obtenir la clé API Gemini",
"openAiApiKey": "Clé API OpenAI",
"openAiBaseUrl": "URL de base",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic API कुंजी",
"getAnthropicApiKey": "Anthropic API कुंजी प्राप्त करें",
"anthropicUseAuthToken": "X-Api-Key के बजाय Anthropic API कुंजी को Authorization हेडर के रूप में पास करें",
"chutesApiKey": "Chutes API कुंजी",
"getChutesApiKey": "Chutes API कुंजी प्राप्त करें",
"deepSeekApiKey": "DeepSeek API कुंजी",
"getDeepSeekApiKey": "DeepSeek API कुंजी प्राप्त करें",
"geminiApiKey": "Gemini API कुंजी",
"getGroqApiKey": "Groq API कुंजी प्राप्त करें",
"groqApiKey": "Groq API कुंजी",
"getGeminiApiKey": "Gemini API कुंजी प्राप्त करें",
"openAiApiKey": "OpenAI API कुंजी",
"openAiBaseUrl": "बेस URL",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Chiave API Anthropic",
"getAnthropicApiKey": "Ottieni chiave API Anthropic",
"anthropicUseAuthToken": "Passa la chiave API Anthropic come header di autorizzazione invece di X-Api-Key",
"chutesApiKey": "Chiave API Chutes",
"getChutesApiKey": "Ottieni chiave API Chutes",
"deepSeekApiKey": "Chiave API DeepSeek",
"getDeepSeekApiKey": "Ottieni chiave API DeepSeek",
"geminiApiKey": "Chiave API Gemini",
"getGroqApiKey": "Ottieni chiave API Groq",
"groqApiKey": "Chiave API Groq",
"getGeminiApiKey": "Ottieni chiave API Gemini",
"openAiApiKey": "Chiave API OpenAI",
"openAiBaseUrl": "URL base",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic APIキー",
"getAnthropicApiKey": "Anthropic APIキーを取得",
"anthropicUseAuthToken": "Anthropic APIキーをX-Api-Keyの代わりにAuthorizationヘッダーとして渡す",
"chutesApiKey": "Chutes APIキー",
"getChutesApiKey": "Chutes APIキーを取得",
"deepSeekApiKey": "DeepSeek APIキー",
"getDeepSeekApiKey": "DeepSeek APIキーを取得",
"geminiApiKey": "Gemini APIキー",
"getGroqApiKey": "Groq APIキーを取得",
"groqApiKey": "Groq APIキー",
"getGeminiApiKey": "Gemini APIキーを取得",
"openAiApiKey": "OpenAI APIキー",
"openAiBaseUrl": "ベースURL",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic API 키",
"getAnthropicApiKey": "Anthropic API 키 받기",
"anthropicUseAuthToken": "X-Api-Key 대신 Authorization 헤더로 Anthropic API 키 전달",
"chutesApiKey": "Chutes API 키",
"getChutesApiKey": "Chutes API 키 받기",
"deepSeekApiKey": "DeepSeek API 키",
"getDeepSeekApiKey": "DeepSeek API 키 받기",
"geminiApiKey": "Gemini API 키",
"getGroqApiKey": "Groq API 키 받기",
"groqApiKey": "Groq API 키",
"getGeminiApiKey": "Gemini API 키 받기",
"openAiApiKey": "OpenAI API 키",
"openAiBaseUrl": "기본 URL",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Klucz API Anthropic",
"getAnthropicApiKey": "Uzyskaj klucz API Anthropic",
"anthropicUseAuthToken": "Przekaż klucz API Anthropic jako nagłówek Authorization zamiast X-Api-Key",
"chutesApiKey": "Klucz API Chutes",
"getChutesApiKey": "Uzyskaj klucz API Chutes",
"deepSeekApiKey": "Klucz API DeepSeek",
"getDeepSeekApiKey": "Uzyskaj klucz API DeepSeek",
"geminiApiKey": "Klucz API Gemini",
"getGroqApiKey": "Uzyskaj klucz API Groq",
"groqApiKey": "Klucz API Groq",
"getGeminiApiKey": "Uzyskaj klucz API Gemini",
"openAiApiKey": "Klucz API OpenAI",
"openAiBaseUrl": "URL bazowy",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Chave de API Anthropic",
"getAnthropicApiKey": "Obter chave de API Anthropic",
"anthropicUseAuthToken": "Passar a chave de API Anthropic como cabeçalho Authorization em vez de X-Api-Key",
"chutesApiKey": "Chave de API Chutes",
"getChutesApiKey": "Obter chave de API Chutes",
"deepSeekApiKey": "Chave de API DeepSeek",
"getDeepSeekApiKey": "Obter chave de API DeepSeek",
"geminiApiKey": "Chave de API Gemini",
"getGroqApiKey": "Obter chave de API Groq",
"groqApiKey": "Chave de API Groq",
"getGeminiApiKey": "Obter chave de API Gemini",
"openAiApiKey": "Chave de API OpenAI",
"openAiBaseUrl": "URL Base",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic API-ключ",
"getAnthropicApiKey": "Получить Anthropic API-ключ",
"anthropicUseAuthToken": "Передавать Anthropic API-ключ как Authorization-заголовок вместо X-Api-Key",
"chutesApiKey": "Chutes API-ключ",
"getChutesApiKey": "Получить Chutes API-ключ",
"deepSeekApiKey": "DeepSeek API-ключ",
"getDeepSeekApiKey": "Получить DeepSeek API-ключ",
"geminiApiKey": "Gemini API-ключ",
"getGroqApiKey": "Получить Groq API-ключ",
"groqApiKey": "Groq API-ключ",
"getGeminiApiKey": "Получить Gemini API-ключ",
"openAiApiKey": "OpenAI API-ключ",
"openAiBaseUrl": "Базовый URL",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic API Anahtarı",
"getAnthropicApiKey": "Anthropic API Anahtarı Al",
"anthropicUseAuthToken": "Anthropic API Anahtarını X-Api-Key yerine Authorization başlığı olarak geçir",
"chutesApiKey": "Chutes API Anahtarı",
"getChutesApiKey": "Chutes API Anahtarı Al",
"deepSeekApiKey": "DeepSeek API Anahtarı",
"getDeepSeekApiKey": "DeepSeek API Anahtarı Al",
"geminiApiKey": "Gemini API Anahtarı",
"getGroqApiKey": "Groq API Anahtarı Al",
"groqApiKey": "Groq API Anahtarı",
"getGeminiApiKey": "Gemini API Anahtarı Al",
"openAiApiKey": "OpenAI API Anahtarı",
"openAiBaseUrl": "Temel URL",

View file

@ -122,9 +122,13 @@
"anthropicApiKey": "Khóa API Anthropic",
"getAnthropicApiKey": "Lấy khóa API Anthropic",
"anthropicUseAuthToken": "Truyền khóa API Anthropic dưới dạng tiêu đề Authorization thay vì X-Api-Key",
"chutesApiKey": "Khóa API Chutes",
"getChutesApiKey": "Lấy khóa API Chutes",
"deepSeekApiKey": "Khóa API DeepSeek",
"getDeepSeekApiKey": "Lấy khóa API DeepSeek",
"geminiApiKey": "Khóa API Gemini",
"getGroqApiKey": "Lấy khóa API Groq",
"groqApiKey": "Khóa API Groq",
"getGeminiApiKey": "Lấy khóa API Gemini",
"openAiApiKey": "Khóa API OpenAI",
"openAiBaseUrl": "URL cơ sở",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic API 密钥",
"getAnthropicApiKey": "获取 Anthropic API 密钥",
"anthropicUseAuthToken": "将 Anthropic API 密钥作为 Authorization 标头传递,而不是 X-Api-Key",
"chutesApiKey": "Chutes API 密钥",
"getChutesApiKey": "获取 Chutes API 密钥",
"deepSeekApiKey": "DeepSeek API 密钥",
"getDeepSeekApiKey": "获取 DeepSeek API 密钥",
"geminiApiKey": "Gemini API 密钥",
"getGroqApiKey": "获取 Groq API 密钥",
"groqApiKey": "Groq API 密钥",
"getGeminiApiKey": "获取 Gemini API 密钥",
"openAiApiKey": "OpenAI API 密钥",
"openAiBaseUrl": "OpenAI 基础 URL",

View file

@ -123,9 +123,13 @@
"anthropicApiKey": "Anthropic API 金鑰",
"getAnthropicApiKey": "取得 Anthropic API 金鑰",
"anthropicUseAuthToken": "將 Anthropic API 金鑰作為 Authorization 標頭傳遞,而非使用 X-Api-Key",
"chutesApiKey": "Chutes API 金鑰",
"getChutesApiKey": "取得 Chutes API 金鑰",
"deepSeekApiKey": "DeepSeek API 金鑰",
"getDeepSeekApiKey": "取得 DeepSeek API 金鑰",
"geminiApiKey": "Gemini API 金鑰",
"getGroqApiKey": "取得 Groq API 金鑰",
"groqApiKey": "Groq API 金鑰",
"getGeminiApiKey": "取得 Gemini API 金鑰",
"openAiApiKey": "OpenAI API 金鑰",
"openAiBaseUrl": "基礎 URL",