mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Adding Groq provider (#4943)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Groq provider support
|
||||
@@ -51,7 +51,7 @@ Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthrop
|
||||
|
||||
### Use any API and Model
|
||||
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, and Cerebras. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
|
||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||
|
||||
|
||||
+9
-3
@@ -23,6 +23,8 @@ service ModelsService {
|
||||
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Updates API configuration
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
// Refreshes and returns Groq models
|
||||
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -120,9 +122,10 @@ enum ApiProvider {
|
||||
XAI = 21;
|
||||
SAMBANOVA = 22;
|
||||
CEREBRAS = 23;
|
||||
SAPAICORE = 24;
|
||||
CLAUDE_CODE = 25;
|
||||
MOONSHOT = 26;
|
||||
GROQ = 24;
|
||||
SAPAICORE = 25;
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -240,4 +243,7 @@ message ModelsApiConfiguration {
|
||||
optional string aws_bedrock_api_key = 75;
|
||||
optional string moonshot_api_key = 76;
|
||||
optional string moonshot_api_line = 77;
|
||||
optional string groq_api_key = 78;
|
||||
optional string groq_model_id = 79;
|
||||
optional OpenRouterModelInfo groq_model_info = 80;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { CerebrasHandler } from "./providers/cerebras"
|
||||
import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -221,6 +222,13 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
cerebrasApiKey: options.cerebrasApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "groq":
|
||||
return new GroqHandler({
|
||||
groqApiKey: options.groqApiKey,
|
||||
groqModelId: options.groqModelId,
|
||||
groqModelInfo: options.groqModelInfo,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
sapAiCoreClientId: options.sapAiCoreClientId,
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { GroqModelId, ModelInfo, groqDefaultModelId, groqModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface GroqHandlerOptions {
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
apiModelId?: string // For backward compatibility
|
||||
}
|
||||
|
||||
// Model family definitions for enhanced behavior
|
||||
interface GroqModelFamily {
|
||||
name: string
|
||||
supportedFeatures: {
|
||||
streaming: boolean
|
||||
temperature: boolean
|
||||
vision: boolean
|
||||
tools: boolean
|
||||
}
|
||||
maxTokensOverride?: number
|
||||
specialParams?: Record<string, any>
|
||||
}
|
||||
|
||||
const MODEL_FAMILIES: Record<string, GroqModelFamily> = {
|
||||
// Moonshort 4 Family - Latest generation with vision support
|
||||
"kimi-k2": {
|
||||
name: "kimi-k2",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
// Llama 4 Family - Latest generation with vision support
|
||||
llama4: {
|
||||
name: "Llama 4",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
// Llama 3.3 Family - Balanced performance
|
||||
"llama3.3": {
|
||||
name: "Llama 3.3",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 32768,
|
||||
},
|
||||
// Llama 3.1 Family - Fast inference
|
||||
"llama3.1": {
|
||||
name: "Llama 3.1",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 131072,
|
||||
},
|
||||
// DeepSeek Family - Reasoning-optimized
|
||||
deepseek: {
|
||||
name: "DeepSeek",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
specialParams: {
|
||||
top_p: 0.95,
|
||||
reasoning_format: "parsed",
|
||||
},
|
||||
},
|
||||
// Qwen Family - Enhanced for Q&A
|
||||
qwen: {
|
||||
name: "Qwen",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 32768,
|
||||
},
|
||||
// Compound Models - Hybrid architectures
|
||||
compound: {
|
||||
name: "Compound",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
}
|
||||
|
||||
export class GroqHandler implements ApiHandler {
|
||||
private options: GroqHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: GroqHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.groqApiKey) {
|
||||
throw new Error("Groq API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: this.options.groqApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Groq client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
const inputTokens = usage?.prompt_tokens || 0
|
||||
const outputTokens = usage?.completion_tokens || 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the model family based on the model ID
|
||||
*/
|
||||
private detectModelFamily(modelId: string): GroqModelFamily {
|
||||
if (modelId.includes("kimi-k2")) {
|
||||
return MODEL_FAMILIES["kimi-k2"]
|
||||
}
|
||||
// Llama 4 variants
|
||||
if (modelId.includes("llama-4") || modelId.includes("llama/llama-4")) {
|
||||
return MODEL_FAMILIES.llama4
|
||||
}
|
||||
// Llama 3.3 variants
|
||||
if (modelId.includes("llama-3.3")) {
|
||||
return MODEL_FAMILIES["llama3.3"]
|
||||
}
|
||||
// Llama 3.1 variants
|
||||
if (modelId.includes("llama-3.1")) {
|
||||
return MODEL_FAMILIES["llama3.1"]
|
||||
}
|
||||
// DeepSeek variants
|
||||
if (modelId.includes("deepseek")) {
|
||||
return MODEL_FAMILIES.deepseek
|
||||
}
|
||||
// Qwen variants
|
||||
if (modelId.includes("qwen")) {
|
||||
return MODEL_FAMILIES.qwen
|
||||
}
|
||||
// Compound variants
|
||||
if (modelId.includes("compound")) {
|
||||
return MODEL_FAMILIES.compound
|
||||
}
|
||||
|
||||
// Default fallback to Llama 3.3 behavior
|
||||
return MODEL_FAMILIES["kimi-k2"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the optimal max_tokens based on model family and capabilities
|
||||
*/
|
||||
private getOptimalMaxTokens(model: { id: string; info: ModelInfo }, modelFamily: GroqModelFamily): number {
|
||||
// Use model-specific max tokens if available
|
||||
if (model.info.maxTokens && model.info.maxTokens > 0) {
|
||||
return model.info.maxTokens
|
||||
}
|
||||
|
||||
// Use family override if available
|
||||
if (modelFamily.maxTokensOverride) {
|
||||
return modelFamily.maxTokensOverride
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 8192
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const modelFamily = this.detectModelFamily(model.id)
|
||||
|
||||
// Optimize parameters based on model family
|
||||
const temperature = 0
|
||||
const maxTokens = this.getOptimalMaxTokens(model, modelFamily)
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
// Build request parameters with model-specific optimizations
|
||||
const requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
|
||||
reasoning_format?: "parsed" | "raw" | "hidden"
|
||||
top_p?: number
|
||||
} = {
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature,
|
||||
}
|
||||
|
||||
// Add any special parameters for specific model families
|
||||
if (modelFamily.specialParams) {
|
||||
Object.assign(requestParams, modelFamily.specialParams)
|
||||
}
|
||||
|
||||
const stream = await client.chat.completions.create(requestParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning field if present (for reasoning models with parsed output)
|
||||
if ((delta as any)?.reasoning) {
|
||||
const reasoningContent = (delta as any).reasoning as string
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle content field - trust the parsed output from Groq
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports vision/images
|
||||
*/
|
||||
supportsImages(): boolean {
|
||||
const model = this.getModel()
|
||||
return model.info.supportsImages === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports tools
|
||||
*/
|
||||
supportsTools(): boolean {
|
||||
const model = this.getModel()
|
||||
const modelFamily = this.detectModelFamily(model.id)
|
||||
return modelFamily.supportedFeatures.tools
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model information with enhanced family detection
|
||||
*/
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
// First priority: groqModelId and groqModelInfo (like Requesty does)
|
||||
const groqModelId = this.options.groqModelId
|
||||
const groqModelInfo = this.options.groqModelInfo
|
||||
if (groqModelId && groqModelInfo) {
|
||||
return { id: groqModelId, info: groqModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: groqModelId with static model info
|
||||
if (groqModelId && groqModelId in groqModels) {
|
||||
const id = groqModelId as GroqModelId
|
||||
return { id, info: groqModels[id] }
|
||||
}
|
||||
|
||||
// Third priority: apiModelId (for backward compatibility)
|
||||
const apiModelId = this.options.apiModelId
|
||||
if (apiModelId && apiModelId in groqModels) {
|
||||
const id = apiModelId as GroqModelId
|
||||
return { id, info: groqModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: groqDefaultModelId,
|
||||
info: groqModels[groqDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model family information for debugging/introspection
|
||||
*/
|
||||
getModelFamily(): GroqModelFamily {
|
||||
const model = this.getModel()
|
||||
return this.detectModelFamily(model.id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import { getAllExtensionState } from "../../storage/state"
|
||||
import { groqModels } from "../../../shared/api"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
|
||||
/**
|
||||
* Refreshes the Groq models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Groq models
|
||||
*/
|
||||
export async function refreshGroqModels(controller: Controller, request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
|
||||
// Get the Groq API key from the controller's state
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
const groqApiKey = apiConfiguration?.groqApiKey
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
try {
|
||||
if (!groqApiKey) {
|
||||
console.log("No Groq API key found, using static models as fallback")
|
||||
// Don't throw an error, just use static models
|
||||
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure the API key is properly formatted
|
||||
const cleanApiKey = groqApiKey.trim()
|
||||
if (!cleanApiKey.startsWith("gsk_")) {
|
||||
throw new Error("Invalid Groq API key format. Groq API keys should start with 'gsk_'")
|
||||
}
|
||||
|
||||
console.log("Fetching Groq models with API key:", cleanApiKey.substring(0, 10) + "...")
|
||||
|
||||
const response = await axios.get("https://api.groq.com/openai/v1/models", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${cleanApiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Cline-VSCode-Extension",
|
||||
},
|
||||
timeout: 10000, // 10 second timeout
|
||||
})
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
// Filter out non-chat models and validate model capabilities
|
||||
if (!isValidChatModel(rawModel)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have static pricing information for this model
|
||||
const staticModelInfo = groqModels[rawModel.id as keyof typeof groqModels]
|
||||
|
||||
const modelInfo: Partial<OpenRouterModelInfo> = {
|
||||
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens || 8192,
|
||||
contextWindow: rawModel.context_window || staticModelInfo?.contextWindow || 8192,
|
||||
supportsImages: detectImageSupport(rawModel, staticModelInfo),
|
||||
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
|
||||
inputPrice: staticModelInfo?.inputPrice || 0,
|
||||
outputPrice: staticModelInfo?.outputPrice || 0,
|
||||
cacheWritesPrice: (staticModelInfo as any)?.cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (staticModelInfo as any).cacheReadsPrice || 0,
|
||||
description: generateModelDescription(rawModel, staticModelInfo),
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid response from Groq API")
|
||||
}
|
||||
await fs.writeFile(groqModelsFilePath, JSON.stringify(models))
|
||||
console.log("Groq models fetched and saved", models)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Groq models:", error)
|
||||
|
||||
// Provide more specific error messages
|
||||
let errorMessage = "Unknown error occurred"
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
errorMessage = "Invalid Groq API key. Please check your API key in settings."
|
||||
} else if (error.response?.status === 403) {
|
||||
errorMessage = "Access forbidden. Please verify your Groq API key has the correct permissions."
|
||||
} else if (error.response?.status === 429) {
|
||||
errorMessage = "Rate limit exceeded. Please try again later."
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timeout. Please check your internet connection."
|
||||
} else {
|
||||
errorMessage = `API request failed: ${error.response?.status || error.code || "Unknown error"}`
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
console.error("Groq API Error:", errorMessage)
|
||||
|
||||
// If we failed to fetch models, try to read cached models first
|
||||
const cachedModels = await readGroqModels(controller)
|
||||
if (cachedModels && Object.keys(cachedModels).length > 0) {
|
||||
console.log("Using cached Groq models")
|
||||
models = cachedModels
|
||||
} else {
|
||||
// Fall back to static models from shared/api.ts
|
||||
console.log("Using static Groq models as fallback")
|
||||
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
|
||||
// by filling in any missing required fields with defaults
|
||||
const typedModels: Record<string, OpenRouterModelInfo> = {}
|
||||
for (const [key, model] of Object.entries(models)) {
|
||||
typedModels[key] = {
|
||||
maxTokens: model.maxTokens ?? 8192,
|
||||
contextWindow: model.contextWindow ?? 8192,
|
||||
supportsImages: model.supportsImages ?? false,
|
||||
supportsPromptCache: model.supportsPromptCache ?? false,
|
||||
inputPrice: model.inputPrice ?? 0,
|
||||
outputPrice: model.outputPrice ?? 0,
|
||||
cacheWritesPrice: model.cacheWritesPrice ?? 0,
|
||||
cacheReadsPrice: model.cacheReadsPrice ?? 0,
|
||||
description: model.description ?? "",
|
||||
tiers: model.tiers ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cached Groq models from disk
|
||||
*/
|
||||
async function readGroqModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
const fileExists = await fileExistsAtPath(groqModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
const fileContents = await fs.readFile(groqModelsFilePath, "utf8")
|
||||
return JSON.parse(fileContents)
|
||||
} catch (error) {
|
||||
console.error("Error reading cached Groq models:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a model is suitable for chat completions
|
||||
*/
|
||||
function isValidChatModel(rawModel: any): boolean {
|
||||
// Check if model is active (if the property exists)
|
||||
if (rawModel.hasOwnProperty("active") && !rawModel.active) {
|
||||
return false
|
||||
}
|
||||
// Filter out non-chat models (whisper, TTS, guard models, etc.)
|
||||
if (
|
||||
rawModel.id.includes("whisper") ||
|
||||
rawModel.id.includes("tts") ||
|
||||
rawModel.id.includes("guard") ||
|
||||
rawModel.id.includes("embedding") ||
|
||||
rawModel.id.includes("moderation") ||
|
||||
rawModel.id.includes("allam")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if model supports chat completions
|
||||
if (rawModel.object === "model" && rawModel.id) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a model supports image input
|
||||
*/
|
||||
function detectImageSupport(rawModel: any, staticModelInfo?: any): boolean {
|
||||
// Use static info if available
|
||||
if (staticModelInfo?.supportsImages !== undefined) {
|
||||
return staticModelInfo.supportsImages
|
||||
}
|
||||
|
||||
// Detect based on model name patterns
|
||||
const modelId = rawModel.id.toLowerCase()
|
||||
if (modelId.includes("vision") || modelId.includes("maverick") || modelId.includes("scout")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a descriptive name for the model
|
||||
*/
|
||||
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
|
||||
// Use static description if available
|
||||
if (staticModelInfo?.description) {
|
||||
return staticModelInfo.description
|
||||
}
|
||||
|
||||
// Generate description based on model characteristics
|
||||
const modelId = rawModel.id
|
||||
const contextWindow = rawModel.context_window || 8192
|
||||
const ownedBy = rawModel.owned_by || "Unknown"
|
||||
|
||||
// Special handling for new models
|
||||
if (modelId.includes("compound")) {
|
||||
return `${ownedBy}'s ${modelId} model with ${contextWindow.toLocaleString()} token context window - Advanced compound architecture`
|
||||
}
|
||||
|
||||
return `${ownedBy} model with ${contextWindow.toLocaleString()} token context window`
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
@@ -42,6 +42,17 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
}
|
||||
})
|
||||
|
||||
handleModelsServiceRequest(controller, "refreshGroqModels", EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// update model info in state for Groq
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
if (apiConfiguration.groqModelId && response.models[apiConfiguration.groqModelId]) {
|
||||
await updateGlobalState(controller.context, "groqModelInfo", response.models[apiConfiguration.groqModelId])
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
|
||||
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
|
||||
// (see normalizeApiConfiguration > openrouter)
|
||||
|
||||
@@ -13,6 +13,7 @@ export const GlobalFileNames = {
|
||||
contextHistory: "context_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
groqModels: "groq_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
|
||||
@@ -26,6 +26,7 @@ export type SecretKey =
|
||||
| "cerebrasApiKey"
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "groqApiKey"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "awsRegion"
|
||||
@@ -117,5 +118,7 @@ export type GlobalStateKey =
|
||||
| "previousModeAwsBedrockCustomSelected"
|
||||
| "previousModeAwsBedrockCustomModelBaseId"
|
||||
| "previousModeSapAiCoreModelId"
|
||||
| "groqModelId"
|
||||
| "groqModelInfo"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
|
||||
|
||||
@@ -167,6 +167,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
planActSeparateModelsSettingRaw,
|
||||
@@ -188,6 +189,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
|
||||
@@ -244,6 +247,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "cerebrasApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "groqApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
@@ -265,6 +269,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
|
||||
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
|
||||
getGlobalState(context, "groqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "groqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
@@ -339,6 +345,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const processingStart = performance.now()
|
||||
let apiProvider: ApiProvider
|
||||
if (storedApiProvider) {
|
||||
// Use the explicitly stored provider - this respects user's selection
|
||||
apiProvider = storedApiProvider
|
||||
} else {
|
||||
// Either new user or legacy user that doesn't have the apiProvider stored in state
|
||||
@@ -442,6 +449,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
@@ -554,6 +564,9 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
@@ -592,6 +605,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
requestyModelInfo,
|
||||
togetherModelId,
|
||||
fireworksModelId,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
sapAiCoreModelId,
|
||||
|
||||
// Global state updates (27 keys)
|
||||
@@ -652,6 +667,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
@@ -696,6 +712,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"xaiApiKey",
|
||||
"sambanovaApiKey",
|
||||
"cerebrasApiKey",
|
||||
"groqApiKey",
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
]
|
||||
|
||||
@@ -28,6 +28,7 @@ export type ApiProvider =
|
||||
| "sambanova"
|
||||
| "cerebras"
|
||||
| "sapaicore"
|
||||
| "groq"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
@@ -99,6 +100,9 @@ export interface ApiHandlerOptions {
|
||||
reasoningEffort?: string
|
||||
sambanovaApiKey?: string
|
||||
cerebrasApiKey?: string
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
requestTimeoutMs?: number
|
||||
sapAiCoreClientId?: string
|
||||
sapAiCoreClientSecret?: string
|
||||
@@ -2403,6 +2407,95 @@ export const cerebrasModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Groq
|
||||
// https://console.groq.com/docs/models
|
||||
// https://groq.com/pricing/
|
||||
export type GroqModelId = keyof typeof groqModels
|
||||
export const groqDefaultModelId: GroqModelId = "moonshotai/kimi-k2-instruct"
|
||||
export const groqModels = {
|
||||
// Compound Beta Models - Hybrid architectures optimized for tool use
|
||||
"compound-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description:
|
||||
"Compound model using Llama 4 Scout for core reasoning with Llama 3.3 70B for routing and tool use. Excellent for plan/act workflows.",
|
||||
},
|
||||
"compound-beta-mini": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description: "Lightweight compound model for faster inference while maintaining tool use capabilities.",
|
||||
},
|
||||
// DeepSeek Models - Reasoning-optimized
|
||||
"deepseek-r1-distill-llama-70b": {
|
||||
maxTokens: 131072,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.75,
|
||||
outputPrice: 0.99,
|
||||
description:
|
||||
"DeepSeek R1 reasoning capabilities distilled into Llama 70B architecture. Excellent for complex problem-solving and planning.",
|
||||
},
|
||||
// Llama 4 Models
|
||||
"meta-llama/llama-4-maverick-17b-128e-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.6,
|
||||
description: "Meta's Llama 4 Maverick 17B model with 128 experts, supports vision and multimodal tasks.",
|
||||
},
|
||||
"meta-llama/llama-4-scout-17b-16e-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.11,
|
||||
outputPrice: 0.34,
|
||||
description: "Meta's Llama 4 Scout 17B model with 16 experts, optimized for fast inference and general tasks.",
|
||||
},
|
||||
// Llama 3.3 Models
|
||||
"llama-3.3-70b-versatile": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.59,
|
||||
outputPrice: 0.79,
|
||||
description: "Meta's latest Llama 3.3 70B model optimized for versatile use cases with excellent performance and speed.",
|
||||
},
|
||||
// Llama 3.1 Models - Fast inference
|
||||
"llama-3.1-8b-instant": {
|
||||
maxTokens: 131072,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.05,
|
||||
outputPrice: 0.08,
|
||||
description: "Fast and efficient Llama 3.1 8B model optimized for speed, low latency, and reliable tool execution.",
|
||||
},
|
||||
// Mistral Models
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
maxTokens: 16384,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 3.0,
|
||||
description:
|
||||
"Kimi K2 is Moonshot AI's state-of-the-art Mixture-of-Experts (MoE) language model with 1 trillion total parameters and 32 billion activated parameters.",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Requesty
|
||||
// https://requesty.ai/models
|
||||
export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest"
|
||||
|
||||
@@ -236,6 +236,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.SAMBANOVA
|
||||
case "cerebras":
|
||||
return ProtoApiProvider.CEREBRAS
|
||||
case "groq":
|
||||
return ProtoApiProvider.GROQ
|
||||
case "sapaicore":
|
||||
return ProtoApiProvider.SAPAICORE
|
||||
case "claude-code":
|
||||
@@ -298,6 +300,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
|
||||
return "sambanova"
|
||||
case ProtoApiProvider.CEREBRAS:
|
||||
return "cerebras"
|
||||
case ProtoApiProvider.GROQ:
|
||||
return "groq"
|
||||
case ProtoApiProvider.SAPAICORE:
|
||||
return "sapaicore"
|
||||
case ProtoApiProvider.CLAUDE_CODE:
|
||||
@@ -378,6 +382,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
sambanovaApiKey: config.sambanovaApiKey,
|
||||
cerebrasApiKey: config.cerebrasApiKey,
|
||||
groqApiKey: config.groqApiKey,
|
||||
groqModelId: config.groqModelId,
|
||||
groqModelInfo: convertModelInfoToProtoOpenRouter(config.groqModelInfo),
|
||||
requestTimeoutMs: config.requestTimeoutMs,
|
||||
apiProvider: config.apiProvider ? convertApiProviderToProto(config.apiProvider) : undefined,
|
||||
favoritedModelIds: config.favoritedModelIds || [],
|
||||
@@ -461,6 +468,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
reasoningEffort: protoConfig.reasoningEffort,
|
||||
sambanovaApiKey: protoConfig.sambanovaApiKey,
|
||||
cerebrasApiKey: protoConfig.cerebrasApiKey,
|
||||
groqApiKey: protoConfig.groqApiKey,
|
||||
groqModelId: protoConfig.groqModelId,
|
||||
groqModelInfo: convertProtoToModelInfo(protoConfig.groqModelInfo),
|
||||
requestTimeoutMs: protoConfig.requestTimeoutMs,
|
||||
apiProvider: protoConfig.apiProvider !== undefined ? convertProtoToApiProvider(protoConfig.apiProvider) : undefined,
|
||||
favoritedModelIds: protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
|
||||
|
||||
@@ -35,6 +35,7 @@ import { LiteLlmProvider } from "./providers/LiteLlmProvider"
|
||||
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
|
||||
import { LMStudioProvider } from "./providers/LMStudioProvider"
|
||||
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
|
||||
import { GroqProvider } from "./providers/GroqProvider"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showSubmitButton?: boolean
|
||||
@@ -129,7 +130,9 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
<VSCodeDropdown
|
||||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={(e: any) => handleFieldChange("apiProvider", e.target.value)}
|
||||
onChange={(e: any) => {
|
||||
handleFieldChange("apiProvider", e.target.value)
|
||||
}}
|
||||
style={{
|
||||
minWidth: 130,
|
||||
position: "relative",
|
||||
@@ -139,12 +142,13 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
||||
<VSCodeOption value="claude-code">Claude Code</VSCodeOption>
|
||||
<VSCodeOption value="bedrock">Amazon Bedrock</VSCodeOption>
|
||||
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
|
||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
|
||||
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
|
||||
<VSCodeOption value="groq">Groq</VSCodeOption>
|
||||
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
|
||||
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
|
||||
<VSCodeOption value="mistral">Mistral</VSCodeOption>
|
||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
|
||||
<VSCodeOption value="requesty">Requesty</VSCodeOption>
|
||||
<VSCodeOption value="fireworks">Fireworks</VSCodeOption>
|
||||
@@ -238,6 +242,9 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
|
||||
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider />}
|
||||
|
||||
{apiConfiguration && selectedProvider === "groq" && (
|
||||
<GroqProvider showModelOptions={showModelOptions} isPopup={isPopup} />
|
||||
)}
|
||||
{apiConfiguration && selectedProvider === "litellm" && (
|
||||
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import Fuse from "fuse.js"
|
||||
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import { useMount } from "react-use"
|
||||
import { groqDefaultModelId, groqModels } from "@shared/api"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "../../services/grpc-client"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView } from "./common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "./utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
|
||||
|
||||
export interface GroqModelPickerProps {
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup }) => {
|
||||
const { apiConfiguration, groqModels: dynamicGroqModels, setGroqModels } = useExtensionState()
|
||||
const { handleFieldsChange } = useApiConfigurationHandlers()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.groqModelId || groqDefaultModelId)
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm)
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
// Use dynamic models if available, otherwise fall back to static models
|
||||
const modelInfo = dynamicGroqModels?.[newModelId] || groqModels[newModelId as keyof typeof groqModels]
|
||||
|
||||
handleFieldsChange({
|
||||
groqModelId: newModelId,
|
||||
groqModelInfo: modelInfo,
|
||||
})
|
||||
setSearchTerm(newModelId)
|
||||
}
|
||||
|
||||
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
useMount(() => {
|
||||
ModelsServiceClient.refreshGroqModels(EmptyRequest.create({}))
|
||||
.then((response) => {
|
||||
setGroqModels({
|
||||
[groqDefaultModelId]: groqModels[groqDefaultModelId],
|
||||
...response.models,
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to refresh Groq models:", err)
|
||||
})
|
||||
})
|
||||
|
||||
// Debounce search term to reduce re-renders
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm)
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsDropdownVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const allGroqModels = useMemo(() => {
|
||||
// Merge static models with dynamic models, with dynamic taking precedence
|
||||
return { ...groqModels, ...(dynamicGroqModels || {}) }
|
||||
}, [dynamicGroqModels])
|
||||
|
||||
const modelIds = useMemo(() => {
|
||||
return Object.keys(allGroqModels).sort((a, b) => a.localeCompare(b))
|
||||
}, [allGroqModels])
|
||||
|
||||
const searchableItems = useMemo(() => {
|
||||
return modelIds.map((id) => ({
|
||||
id,
|
||||
html: id,
|
||||
}))
|
||||
}, [modelIds])
|
||||
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(searchableItems, {
|
||||
keys: ["html"], // highlight function will update this
|
||||
threshold: 0.6,
|
||||
shouldSort: true,
|
||||
isCaseSensitive: false,
|
||||
ignoreLocation: false,
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 1,
|
||||
})
|
||||
}, [searchableItems])
|
||||
|
||||
const modelSearchResults = useMemo(() => {
|
||||
let results: { id: string; html: string }[] = debouncedSearchTerm
|
||||
? highlight(fuse.search(debouncedSearchTerm), "model-item-highlight")
|
||||
: searchableItems
|
||||
return results
|
||||
}, [searchableItems, debouncedSearchTerm, fuse])
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (!isDropdownVisible) return
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowDown":
|
||||
event.preventDefault()
|
||||
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
|
||||
break
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
|
||||
break
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
|
||||
handleModelChange(modelSearchResults[selectedIndex].id)
|
||||
setIsDropdownVisible(false)
|
||||
}
|
||||
break
|
||||
case "Escape":
|
||||
setIsDropdownVisible(false)
|
||||
setSelectedIndex(-1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const hasInfo = useMemo(() => {
|
||||
try {
|
||||
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [modelIds, searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(-1)
|
||||
if (dropdownListRef.current) {
|
||||
dropdownListRef.current.scrollTop = 0
|
||||
}
|
||||
}, [searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
|
||||
itemRefs.current[selectedIndex]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
}, [selectedIndex])
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<style>
|
||||
{`
|
||||
.model-item-highlight {
|
||||
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||
color: inherit;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="model-search">
|
||||
<span className="font-medium">Model</span>
|
||||
</label>
|
||||
<div ref={dropdownRef} className="relative w-full">
|
||||
<VSCodeTextField
|
||||
id="model-search"
|
||||
placeholder="Search and select a model..."
|
||||
value={searchTerm}
|
||||
onInput={(e) => {
|
||||
setSearchTerm((e.target as HTMLInputElement)?.value || "")
|
||||
setIsDropdownVisible(true)
|
||||
}}
|
||||
onFocus={() => setIsDropdownVisible(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
width: "100%",
|
||||
zIndex: GROQ_MODEL_PICKER_Z_INDEX,
|
||||
position: "relative",
|
||||
}}>
|
||||
{searchTerm && (
|
||||
<div
|
||||
className="input-icon-button codicon codicon-close flex justify-center items-center h-full"
|
||||
aria-label="Clear search"
|
||||
onClick={() => {
|
||||
setSearchTerm("")
|
||||
setIsDropdownVisible(true)
|
||||
}}
|
||||
slot="end"
|
||||
/>
|
||||
)}
|
||||
</VSCodeTextField>
|
||||
{isDropdownVisible && (
|
||||
<div
|
||||
ref={dropdownListRef}
|
||||
className="absolute top-[calc(100%-3px)] left-0 w-[calc(100%-2px)] max-h-[200px] overflow-y-auto border border-[var(--vscode-list-activeSelectionBackground)] rounded-b-[3px]"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
zIndex: GROQ_MODEL_PICKER_Z_INDEX - 1,
|
||||
}}>
|
||||
{modelSearchResults.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
ref={(el: HTMLDivElement | null) => (itemRefs.current[index] = el)}
|
||||
className={`px-2.5 py-1.5 cursor-pointer break-all whitespace-normal hover:bg-[var(--vscode-list-activeSelectionBackground)] ${
|
||||
index === selectedIndex ? "bg-[var(--vscode-list-activeSelectionBackground)]" : ""
|
||||
}`}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
onClick={() => {
|
||||
handleModelChange(item.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.html,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasInfo ? (
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
) : (
|
||||
<p className="text-xs mt-0 text-[var(--vscode-descriptionForeground)]">
|
||||
<>
|
||||
The extension automatically fetches the latest list of models available on{" "}
|
||||
<VSCodeLink className="inline text-inherit" href="https://console.groq.com/docs/models">
|
||||
Groq.
|
||||
</VSCodeLink>
|
||||
If you're unsure which model to choose, Cline works best with{" "}
|
||||
<VSCodeLink className="inline text-inherit" onClick={() => handleModelChange("llama-3.3-70b-versatile")}>
|
||||
llama-3.3-70b-versatile.
|
||||
</VSCodeLink>
|
||||
</>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const GROQ_MODEL_PICKER_Z_INDEX = 1_000
|
||||
|
||||
export default GroqModelPicker
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import GroqModelPicker from "../GroqModelPicker"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
/**
|
||||
* Props for the GroqProvider component
|
||||
*/
|
||||
interface GroqProviderProps {
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Groq provider configuration component
|
||||
*/
|
||||
export const GroqProvider = ({ showModelOptions, isPopup }: GroqProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
initialValue={apiConfiguration?.groqApiKey || ""}
|
||||
onChange={(value) => handleFieldChange("groqApiKey", value)}
|
||||
providerName="Groq"
|
||||
signupUrl="https://console.groq.com/keys"
|
||||
/>
|
||||
|
||||
{showModelOptions && <GroqModelPicker isPopup={isPopup} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -44,6 +44,8 @@ import {
|
||||
sapAiCoreDefaultModelId,
|
||||
claudeCodeDefaultModelId,
|
||||
claudeCodeModels,
|
||||
groqModels,
|
||||
groqDefaultModelId,
|
||||
} from "@shared/api"
|
||||
|
||||
/**
|
||||
@@ -178,6 +180,14 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
|
||||
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
|
||||
case "cerebras":
|
||||
return getProviderData(cerebrasModels, cerebrasDefaultModelId)
|
||||
case "groq":
|
||||
const result = {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: apiConfiguration?.groqModelId || groqDefaultModelId,
|
||||
selectedModelInfo: apiConfiguration?.groqModelInfo || groqModels[groqDefaultModelId],
|
||||
}
|
||||
|
||||
return result
|
||||
case "sapaicore":
|
||||
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId)
|
||||
default:
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
openRouterDefaultModelInfo,
|
||||
requestyDefaultModelId,
|
||||
requestyDefaultModelInfo,
|
||||
groqDefaultModelId,
|
||||
groqModels,
|
||||
} from "../../../src/shared/api"
|
||||
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
@@ -38,6 +40,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
openRouterModels: Record<string, ModelInfo>
|
||||
openAiModels: string[]
|
||||
requestyModels: Record<string, ModelInfo>
|
||||
groqModels: Record<string, ModelInfo>
|
||||
mcpServers: McpServer[]
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
@@ -58,6 +61,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
setRequestyModels: (value: Record<string, ModelInfo>) => void
|
||||
setGroqModels: (value: Record<string, ModelInfo>) => void
|
||||
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
@@ -205,6 +209,9 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
})
|
||||
const [groqModelsState, setGroqModels] = useState<Record<string, ModelInfo>>({
|
||||
[groqDefaultModelId]: groqModels[groqDefaultModelId],
|
||||
})
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
|
||||
|
||||
@@ -249,7 +256,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
if (response.stateJson) {
|
||||
try {
|
||||
const stateData = JSON.parse(response.stateJson) as ExtensionState
|
||||
console.log("[DEBUG] parsed state JSON, updating state")
|
||||
setState((prevState) => {
|
||||
// Versioning logic for autoApprovalSettings
|
||||
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
|
||||
@@ -631,6 +637,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
openRouterModels,
|
||||
openAiModels,
|
||||
requestyModels,
|
||||
groqModels: groqModelsState,
|
||||
mcpServers,
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
@@ -670,6 +677,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setRequestyModels: (models: Record<string, ModelInfo>) => setRequestyModels(models),
|
||||
setGroqModels: (models: Record<string, ModelInfo>) => setGroqModels(models),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setShowMcp,
|
||||
closeMcpView,
|
||||
|
||||
Reference in New Issue
Block a user