From eaa76512fca1c2dc0331c563620c103e08cb453f Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:23:24 -0800 Subject: [PATCH] Remove requesty polling; fix deepseek cost calculation changes; fix preferred language parsing --- src/api/providers/deepseek.ts | 42 +-- src/api/providers/openai-native.ts | 51 ++-- src/api/providers/openai.ts | 102 ++----- src/api/providers/requesty.ts | 26 +- src/core/Cline.ts | 33 +-- src/core/webview/ClineProvider.ts | 119 ++------ src/shared/ExtensionMessage.ts | 2 - src/shared/WebviewMessage.ts | 1 - src/shared/api.ts | 17 -- webview-ui/eslint.config.js | 7 +- .../src/components/chat/ChatTextArea.tsx | 6 +- .../src/components/settings/ApiOptions.tsx | 53 ++-- .../settings/OpenRouterModelPicker.tsx | 159 +++++++++- .../settings/RequestyModelPicker.tsx | 274 ------------------ .../src/components/settings/SettingsView.tsx | 3 +- .../src/context/ExtensionStateContext.tsx | 23 +- webview-ui/src/main.tsx | 2 +- webview-ui/src/utils/validate.ts | 19 +- 18 files changed, 289 insertions(+), 650 deletions(-) delete mode 100644 webview-ui/src/components/settings/RequestyModelPicker.tsx diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 35373b7cce..9049e646db 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -3,7 +3,6 @@ import OpenAI from "openai" import { withRetry } from "../retry" import { ApiHandler } from "../" import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api" -import { calculateApiCostOpenAI } from "../../utils/cost" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" import { convertToR1Format } from "../transform/r1-format" @@ -20,37 +19,6 @@ export class DeepSeekHandler implements ApiHandler { }) } - private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream { - // Deepseek reports total input AND cache reads/writes, - // see context caching: https://api-docs.deepseek.com/guides/kv_cache) - // where the input tokens is the sum of the cache hits/misses, just like OpenAI. - // This affects: - // 1) context management truncation algorithm, and - // 2) cost calculation - - // Deepseek usage includes extra fields. - // Safely cast the prompt token details section to the appropriate structure. - interface DeepSeekUsage extends OpenAI.CompletionUsage { - prompt_cache_hit_tokens?: number - prompt_cache_miss_tokens?: number - } - const deepUsage = usage as DeepSeekUsage - - const inputTokens = deepUsage?.prompt_tokens || 0 - const outputTokens = deepUsage?.completion_tokens || 0 - const cacheReadTokens = deepUsage?.prompt_cache_hit_tokens || 0 - const cacheWriteTokens = deepUsage?.prompt_cache_miss_tokens || 0 - const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) - yield { - type: "usage", - inputTokens: inputTokens, - outputTokens: outputTokens, - cacheWriteTokens: cacheWriteTokens, - cacheReadTokens: cacheReadTokens, - totalCost: totalCost, - } - } - @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const model = this.getModel() @@ -93,7 +61,15 @@ export class DeepSeekHandler implements ApiHandler { } if (chunk.usage) { - yield* this.yieldUsage(model.info, chunk.usage) + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses) + outputTokens: chunk.usage.completion_tokens || 0, + // @ts-ignore-next-line + cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0, + // @ts-ignore-next-line + cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0, + } } } } diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 8fadc60485..c90aa6f862 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -10,7 +10,6 @@ import { openAiNativeModels, } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" -import { calculateApiCostOpenAI } from "../../utils/cost" import { ApiStream } from "../transform/stream" import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs" @@ -25,47 +24,31 @@ export class OpenAiNativeHandler implements ApiHandler { }) } - private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream { - const inputTokens = usage?.prompt_tokens || 0 - const outputTokens = usage?.completion_tokens || 0 - const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0 - const cacheWriteTokens = 0 - const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) - yield { - type: "usage", - inputTokens: inputTokens, - outputTokens: outputTokens, - cacheWriteTokens: cacheWriteTokens, - cacheReadTokens: cacheReadTokens, - totalCost: totalCost, - } - } - @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { - const model = this.getModel() - - switch (model.id) { + switch (this.getModel().id) { case "o1": case "o1-preview": case "o1-mini": { // o1 doesnt support streaming, non-1 temp, or system prompt const response = await this.client.chat.completions.create({ - model: model.id, + model: this.getModel().id, messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)], }) yield { type: "text", text: response.choices[0]?.message.content || "", } - - yield* this.yieldUsage(model.info, response.usage) - + yield { + type: "usage", + inputTokens: response.usage?.prompt_tokens || 0, + outputTokens: response.usage?.completion_tokens || 0, + } break } case "o3-mini": { const stream = await this.client.chat.completions.create({ - model: model.id, + model: this.getModel().id, messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, @@ -80,15 +63,18 @@ export class OpenAiNativeHandler implements ApiHandler { } } if (chunk.usage) { - // Only last chunk contains usage - yield* this.yieldUsage(model.info, chunk.usage) + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } } } break } default: { const stream = await this.client.chat.completions.create({ - model: model.id, + model: this.getModel().id, // max_completion_tokens: this.getModel().info.maxTokens, temperature: 0, messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], @@ -104,9 +90,14 @@ export class OpenAiNativeHandler implements ApiHandler { text: delta.content, } } + + // contains a null value except for the last chunk which contains the token usage statistics for the entire request if (chunk.usage) { - // Only last chunk contains usage - yield* this.yieldUsage(model.info, chunk.usage) + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } } } } diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index de514c6b65..7309d5bb98 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -28,65 +28,6 @@ export class OpenAiHandler implements ApiHandler { } } - private async diagnoseRequestProblem( - modelId: string, - messages: OpenAI.Chat.ChatCompletionMessageParam[], - apiKey: string, - baseURL: string, - ) { - const url = `${baseURL}/chat/completions` - - try { - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - model: modelId, - messages: messages, - temperature: 0, - stream: true, - }), - }) - - if (!response.ok) { - return `HTTP error! status: ${response.status}, statusText: ${response.statusText}` - } - - const responseData = await response.json() - return responseData - } catch (error) { - return error instanceof Error ? error.message : String(error) - } - } - - private async *handleChunk(chunk: OpenAI.Chat.Completions.ChatCompletionChunk): ApiStream { - const delta = chunk.choices[0]?.delta - if (delta?.content) { - yield { - type: "text", - text: delta.content, - } - } - - if (delta && "reasoning_content" in delta && delta.reasoning_content) { - yield { - type: "reasoning", - reasoning: (delta.reasoning_content as string | undefined) || "", - } - } - - if (chunk.usage) { - yield { - type: "usage", - inputTokens: chunk.usage.prompt_tokens || 0, - outputTokens: chunk.usage.completion_tokens || 0, - } - } - } - @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const modelId = this.options.openAiModelId ?? "" @@ -108,30 +49,29 @@ export class OpenAiHandler implements ApiHandler { stream: true, stream_options: { include_usage: true }, }) + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } - const [validationStream, contentStream] = stream.tee() + if (delta && "reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + reasoning: (delta.reasoning_content as string | undefined) || "", + } + } - // Check the first chunk to detect potential stream issues early - // This helps to provide better error messages for cases like: - // https://github.com/cline/cline/issues/1662 - // where the stream appears valid but contains no actual data - const firstChunk = await validationStream[Symbol.asyncIterator]().next() - if (firstChunk.done || !firstChunk.value) { - // Make an additional request to get detailed error information - // This gives us more context about what went wrong with the API call - const errorResponse = await this.diagnoseRequestProblem( - modelId, - openAiMessages, - this.client.apiKey, - this.client.baseURL, - ) - throw new Error(`Stream empty. Error details: ${JSON.stringify(errorResponse)}`) - } - - yield* this.handleChunk(firstChunk.value) - - for await (const chunk of contentStream) { - yield* this.handleChunk(chunk) + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } } } diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 14bdd35d17..f252b89e69 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -1,15 +1,8 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { withRetry } from "../retry" -import { calculateApiCostOpenAI } from "../../utils/cost" -import { - ApiHandlerOptions, - ModelInfo, - openAiModelInfoSaneDefaults, - requestyDefaultModelId, - requestyDefaultModelInfo, -} from "../../shared/api" +import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" import { ApiHandler } from "../index" +import { withRetry } from "../retry" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" @@ -31,7 +24,7 @@ export class RequestyHandler implements ApiHandler { @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { - const model = this.getModel() + const modelId = this.options.requestyModelId ?? "" let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: "system", content: systemPrompt }, @@ -40,13 +33,12 @@ export class RequestyHandler implements ApiHandler { // @ts-ignore-next-line const stream = await this.client.chat.completions.create({ - model: model.id, - max_tokens: model.info.maxTokens || undefined, + model: modelId, messages: openAiMessages, temperature: 0, stream: true, stream_options: { include_usage: true }, - ...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}), + ...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}), }) for await (const chunk of stream) { @@ -96,11 +88,9 @@ export class RequestyHandler implements ApiHandler { } getModel(): { id: string; info: ModelInfo } { - const modelId = this.options.requestyModelId - const modelInfo = this.options.requestyModelInfo - if (modelId && modelInfo) { - return { id: modelId, info: modelInfo } + return { + id: this.options.requestyModelId ?? "", + info: openAiModelInfoSaneDefaults, } - return { id: requestyDefaultModelId, info: requestyDefaultModelInfo } } } diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 0fbc3faeac..79abc41bcd 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -75,7 +75,6 @@ export class Cline { browserSession: BrowserSession private didEditFile: boolean = false customInstructions?: string - preferredLanguage?: LanguageKey autoApprovalSettings: AutoApprovalSettings private browserSettings: BrowserSettings private chatSettings: ChatSettings @@ -136,9 +135,6 @@ export class Cline { this.browserSession = new BrowserSession(provider.context, browserSettings) this.diffViewProvider = new DiffViewProvider(cwd) this.customInstructions = customInstructions - this.preferredLanguage = getLanguageKey( - vscode.workspace.getConfiguration("cline").get("preferredLanguage"), - ) this.autoApprovalSettings = autoApprovalSettings this.browserSettings = browserSettings this.chatSettings = chatSettings @@ -152,16 +148,6 @@ export class Cline { } else { throw new Error("Either historyItem or task/images must be provided") } - // capture start of thread with the state at the beginning - telemetryService.capture({ - event: "cline created", - properties: { - taskId: this.taskId, - isHistory: !!historyItem, - chatMode: this.chatSettings.mode, - hasImages: !!images, - }, - }) } updateBrowserSettings(browserSettings: BrowserSettings) { @@ -1292,9 +1278,12 @@ export class Cline { let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, mcpHub, this.browserSettings) let settingsCustomInstructions = this.customInstructions?.trim() + const preferredLanguage = getLanguageKey( + vscode.workspace.getConfiguration("cline").get("preferredLanguage"), + ) const preferredLanguageInstructions = - this.preferredLanguage && this.preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS - ? `# Preferred Language\n\nSpeak in ${this.preferredLanguage}.` + preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS + ? `# Preferred Language\n\nSpeak in ${preferredLanguage}.` : "" const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules) let clineRulesFileInstructions: string | undefined @@ -1318,8 +1307,8 @@ export class Cline { if ( settingsCustomInstructions || clineRulesFileInstructions || - preferredLanguageInstructions || - clineIgnoreInstructions + clineIgnoreInstructions || + preferredLanguageInstructions ) { // altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with systemPrompt += addUserInstructions( @@ -3311,14 +3300,6 @@ export class Cline { this.consecutiveMistakeCount++ } - telemetryService.capture({ - event: "message sent", - properties: { - taskId: this.taskId, - chatMode: this.chatSettings.mode, - }, - }) - const recDidEndLoop = await this.recursivelyMakeClineRequests(this.userMessageContent) didEndLoop = recDidEndLoop } else { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d5b9440bc1..a963e3dd9b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -96,7 +96,6 @@ type GlobalStateKey = | "liteLlmModelId" | "qwenApiLine" | "requestyModelId" - | "requestyModelInfo" | "togetherModelId" | "mcpMarketplaceCatalog" | "telemetrySetting" @@ -105,7 +104,6 @@ export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", uiMessages: "ui_messages.json", openRouterModels: "openrouter_models.json", - requestyModels: "requesty_models.json", mcpSettings: "cline_mcp_settings.json", clineRules: ".clinerules", } @@ -513,7 +511,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { }), ) // post last cached models in case the call to endpoint fails - this.readDynamicProviderModels(GlobalFileNames.openRouterModels).then((openRouterModels) => { + this.readOpenRouterModels().then((openRouterModels) => { if (openRouterModels) { this.postMessageToWebview({ type: "openRouterModels", @@ -554,36 +552,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { const { telemetrySetting } = state const isOptedIn = telemetrySetting === "enabled" telemetryService.updateTelemetryState(isOptedIn) - - // only fetch requesty api key if api key is set - if (state.apiConfiguration?.requestyApiKey) { - // post last cached models in case the call to endpoint fails - this.readDynamicProviderModels(GlobalFileNames.requestyModels).then((requestyModels) => { - if (requestyModels) { - this.postMessageToWebview({ - type: "requestyModels", - requestyModels, - }) - } - }) - - // 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) - this.refreshRequestyModels().then(async (requestyModels) => { - if (requestyModels) { - // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) - const { apiConfiguration } = await this.getState() - if (apiConfiguration.requestyModelId) { - await this.updateGlobalState( - "requestyModelInfo", - requestyModels[apiConfiguration.requestyModelId], - ) - await this.postStateToWebview() - } - } - }) - } }) break @@ -628,7 +596,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { deepSeekApiKey, requestyApiKey, requestyModelId, - requestyModelInfo, togetherApiKey, togetherModelId, qwenApiKey, @@ -682,7 +649,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("liteLlmModelId", liteLlmModelId) await this.updateGlobalState("qwenApiLine", qwenApiLine) await this.updateGlobalState("requestyModelId", requestyModelId) - await this.updateGlobalState("requestyModelInfo", requestyModelInfo) await this.updateGlobalState("togetherModelId", togetherModelId) if (this.cline) { this.cline.api = buildApiHandler(message.apiConfiguration) @@ -779,9 +745,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "refreshOpenRouterModels": await this.refreshOpenRouterModels() break - case "refreshRequestyModels": - await this.refreshRequestyModels() - break case "refreshOpenAiModels": const { apiConfiguration } = await this.getState() const openAiModels = await this.getOpenAiModels( @@ -1056,10 +1019,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId) await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo) break - case "requesty": - await this.updateGlobalState("previousModeModelId", apiConfiguration.requestyModelId) - await this.updateGlobalState("previousModeModelInfo", apiConfiguration.requestyModelInfo) - break case "vscode-lm": await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector) break @@ -1076,6 +1035,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "litellm": await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId) break + case "requesty": + await this.updateGlobalState("previousModeModelId", apiConfiguration.requestyModelId) + break } // Restore the model used in previous mode @@ -1092,10 +1054,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("openRouterModelId", newModelId) await this.updateGlobalState("openRouterModelInfo", newModelInfo) break - case "requesty": - await this.updateGlobalState("requestyModelId", newModelId) - await this.updateGlobalState("requestyModelInfo", newModelInfo) - break case "vscode-lm": await this.updateGlobalState("vsCodeLmModelSelector", newModelId) break @@ -1112,6 +1070,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "litellm": await this.updateGlobalState("liteLlmModelId", newModelId) break + case "requesty": + await this.updateGlobalState("requestyModelId", newModelId) + break } if (this.cline) { @@ -1568,61 +1529,16 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont return cacheDir } - async readDynamicProviderModels(filename: string): Promise | undefined> { - const filePath = path.join(await this.ensureCacheDirectoryExists(), filename) - const fileExists = await fileExistsAtPath(filePath) + async readOpenRouterModels(): Promise | undefined> { + const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels) + const fileExists = await fileExistsAtPath(openRouterModelsFilePath) if (fileExists) { - const fileContents = await fs.readFile(filePath, "utf8") + const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8") return JSON.parse(fileContents) } return undefined } - adjustPriceToMillionTokens(price: any) { - if (price) { - return parseFloat(price) * 1_000_000 - } - return undefined - } - - async refreshRequestyModels() { - const requestyModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.requestyModels) - - let models: Record = {} - try { - const response = await axios.get("https://router.requesty.ai/v1/models") - if (response.data?.data) { - for (const model of response.data.data) { - const modelInfo: ModelInfo = { - maxTokens: model.max_output_tokens, - contextWindow: model.context_window, - supportsImages: model.supports_images || undefined, - supportsComputerUse: model.supports_computer_use || undefined, - supportsPromptCache: model.supports_caching || undefined, - inputPrice: this.adjustPriceToMillionTokens(model.input_price), - outputPrice: this.adjustPriceToMillionTokens(model.output_price), - cacheWritesPrice: this.adjustPriceToMillionTokens(model.caching_price), - cacheReadsPrice: this.adjustPriceToMillionTokens(model.cached_price), - description: model.description, - } - models[model.id] = modelInfo - } - await fs.writeFile(requestyModelsFilePath, JSON.stringify(models)) - console.log("Requesty models fetched and saved", models) - } else { - console.error("Invalid response from Requesty API") - } - } catch (error) { - console.error("Error fetching Requesty models:", error) - } - - await this.postMessageToWebview({ - type: "requestyModels", - requestyModels: models, - }) - return models - } - async refreshOpenRouterModels() { const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels) @@ -1657,14 +1573,20 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont */ if (response.data?.data) { const rawModels = response.data.data + const parsePrice = (price: any) => { + if (price) { + return parseFloat(price) * 1_000_000 + } + return undefined + } for (const rawModel of rawModels) { const modelInfo: ModelInfo = { maxTokens: rawModel.top_provider?.max_completion_tokens, contextWindow: rawModel.context_length, supportsImages: rawModel.architecture?.modality?.includes("image"), supportsPromptCache: false, - inputPrice: this.adjustPriceToMillionTokens(rawModel.pricing?.prompt), - outputPrice: this.adjustPriceToMillionTokens(rawModel.pricing?.completion), + inputPrice: parsePrice(rawModel.pricing?.prompt), + outputPrice: parsePrice(rawModel.pricing?.completion), description: rawModel.description, } @@ -1965,7 +1887,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont deepSeekApiKey, requestyApiKey, requestyModelId, - requestyModelInfo, togetherApiKey, togetherModelId, qwenApiKey, @@ -2019,7 +1940,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont this.getSecret("deepSeekApiKey") as Promise, this.getSecret("requestyApiKey") as Promise, this.getGlobalState("requestyModelId") as Promise, - this.getGlobalState("requestyModelInfo") as Promise, this.getSecret("togetherApiKey") as Promise, this.getGlobalState("togetherModelId") as Promise, this.getSecret("qwenApiKey") as Promise, @@ -2100,7 +2020,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont deepSeekApiKey, requestyApiKey, requestyModelId, - requestyModelInfo, togetherApiKey, togetherModelId, qwenApiKey, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 9b0ee7d17c..8924e7adad 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -22,7 +22,6 @@ export interface ExtensionMessage { | "invoke" | "partialMessage" | "openRouterModels" - | "requestyModels" | "openAiModels" | "mcpServers" | "relinquishControl" @@ -52,7 +51,6 @@ export interface ExtensionMessage { filePaths?: string[] partialMessage?: ClineMessage openRouterModels?: Record - requestyModels?: Record openAiModels?: string[] mcpServers?: McpServer[] mcpMarketplaceCatalog?: McpMarketplaceCatalog diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 7b72f1ccd4..897589c71a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -27,7 +27,6 @@ export interface WebviewMessage { | "openMention" | "cancelTask" | "refreshOpenRouterModels" - | "refreshRequestyModels" | "refreshOpenAiModels" | "openMcpSettings" | "restartMcpServer" diff --git a/src/shared/api.ts b/src/shared/api.ts index e9acc8a8c5..7a052a8d5e 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -49,7 +49,6 @@ export interface ApiHandlerOptions { deepSeekApiKey?: string requestyApiKey?: string requestyModelId?: string - requestyModelInfo?: ModelInfo togetherApiKey?: string togetherModelId?: string qwenApiKey?: string @@ -804,22 +803,6 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = { outputPrice: 0, } -// Requesty -// https://requesty.ai/models -export const requestyDefaultModelId = "anthropic/claude-3-5-sonnet-latest" -export const requestyDefaultModelInfo: ModelInfo = { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsComputerUse: false, - supportsPromptCache: true, - inputPrice: 3.0, - outputPrice: 15.0, - cacheWritesPrice: 3.75, - cacheReadsPrice: 0.3, - description: "Anthropic's most intelligent model. Highest level of intelligence and capability.", -} - // X AI // https://docs.x.ai/docs/api-reference export type XAIModelId = keyof typeof xaiModels diff --git a/webview-ui/eslint.config.js b/webview-ui/eslint.config.js index b1474997b2..cecacb1595 100644 --- a/webview-ui/eslint.config.js +++ b/webview-ui/eslint.config.js @@ -19,8 +19,13 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + // "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-object-type": "off", + "no-case-declarations": "off", + "react-hooks/exhaustive-deps": "off", + "prefer-const": "off", }, }, ) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index ac1de348d0..2e99130d57 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -214,7 +214,7 @@ const ChatTextArea = forwardRef( }, ref, ) => { - const { filePaths, chatSettings, apiConfiguration, openRouterModels, requestyModels, platform } = useExtensionState() + const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState() const [isTextAreaFocused, setIsTextAreaFocused] = useState(false) const [gitCommits, setGitCommits] = useState([]) @@ -635,14 +635,14 @@ const ChatTextArea = forwardRef( // Separate the API config submission logic const submitApiConfig = useCallback(() => { const apiValidationResult = validateApiConfiguration(apiConfiguration) - const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels, requestyModels) + const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels) if (!apiValidationResult && !modelIdValidationResult) { vscode.postMessage({ type: "apiConfiguration", apiConfiguration }) } else { vscode.postMessage({ type: "getLatestState" }) } - }, [apiConfiguration, openRouterModels, requestyModels]) + }, [apiConfiguration, openRouterModels]) const onModeToggle = useCallback(() => { // if (textAreaDisabled) return diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index d801ef06b5..f05cb3d56f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -10,30 +10,30 @@ import { import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react" import ThinkingBudgetSlider from "./ThinkingBudgetSlider" import { useEvent, useInterval } from "react-use" +import styled from "styled-components" +import * as vscodemodels from "vscode" import { - ApiConfiguration, - ApiProvider, - ModelInfo, anthropicDefaultModelId, anthropicModels, + ApiConfiguration, + ApiProvider, azureOpenAiDefaultApiVersion, bedrockDefaultModelId, bedrockModels, deepSeekDefaultModelId, deepSeekModels, - qwenDefaultModelId, - qwenModels, geminiDefaultModelId, geminiModels, mistralDefaultModelId, mistralModels, + ModelInfo, openAiModelInfoSaneDefaults, openAiNativeDefaultModelId, openAiNativeModels, openRouterDefaultModelId, openRouterDefaultModelInfo, - requestyDefaultModelId, - requestyDefaultModelInfo, + qwenDefaultModelId, + qwenModels, vertexDefaultModelId, vertexModels, xaiDefaultModelId, @@ -42,13 +42,9 @@ import { import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" -import VSCodeButtonLink from "../common/VSCodeButtonLink" -import OpenRouterModelPicker from "./OpenRouterModelPicker" -import RequestyModelPicker from "./RequestyModelPicker" -import ModelDescriptionMarkdown from "./ModelDescriptionMarkdown" -import styled from "styled-components" -import * as vscodemodels from "vscode" import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles" +import VSCodeButtonLink from "../common/VSCodeButtonLink" +import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker" interface ApiOptionsProps { showModelOptions: boolean @@ -58,7 +54,7 @@ interface ApiOptionsProps { } // This is necessary to ensure dropdown opens downward, important for when this is used in popup -const DROPDOWN_Z_INDEX = 1001 // Higher than the Requesty/OpenRouterModelPicker's and ModelSelectorTooltip's z-index +const DROPDOWN_Z_INDEX = 1001 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index const DropdownContainer = styled.div<{ zIndex?: number }>` position: relative; @@ -849,7 +845,24 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is placeholder="Enter API Key..."> API Key - {!apiConfiguration?.requestyApiKey && Get API Key} + + Model ID + +

+ + (Note: Cline uses complex prompts and works best with Claude + models. Less capable models may not work as expected.) + +

)} @@ -1165,11 +1178,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is )} {selectedProvider !== "openrouter" && - selectedProvider !== "requesty" && selectedProvider !== "openai" && selectedProvider !== "ollama" && selectedProvider !== "lmstudio" && selectedProvider !== "vscode-lm" && + selectedProvider !== "litellm" && + selectedProvider !== "requesty" && showModelOptions && ( <> @@ -1202,7 +1216,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is )} {selectedProvider === "openrouter" && showModelOptions && } - {selectedProvider === "requesty" && showModelOptions && } {modelIdErrorMessage && (

` background-color: var(--vscode-list-activeSelectionBackground); } ` + +// Markdown + +const StyledMarkdown = styled.div` + font-family: + var(--vscode-font-family), + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Oxygen, + Ubuntu, + Cantarell, + "Open Sans", + "Helvetica Neue", + sans-serif; + font-size: 12px; + color: var(--vscode-descriptionForeground); + + p, + li, + ol, + ul { + line-height: 1.25; + margin: 0; + } + + ol, + ul { + padding-left: 1.5em; + margin-left: 0; + } + + p { + white-space: pre-wrap; + } + + a { + text-decoration: none; + } + a { + &:hover { + text-decoration: underline; + } + } +` + +export const ModelDescriptionMarkdown = memo( + ({ + markdown, + key, + isExpanded, + setIsExpanded, + isPopup, + }: { + markdown?: string + key: string + isExpanded: boolean + setIsExpanded: (isExpanded: boolean) => void + isPopup?: boolean + }) => { + const [reactContent, setMarkdown] = useRemark() + // const [isExpanded, setIsExpanded] = useState(false) + const [showSeeMore, setShowSeeMore] = useState(false) + const textContainerRef = useRef(null) + const textRef = useRef(null) + + useEffect(() => { + setMarkdown(markdown || "") + }, [markdown, setMarkdown]) + + useEffect(() => { + if (textRef.current && textContainerRef.current) { + const { scrollHeight } = textRef.current + const { clientHeight } = textContainerRef.current + const isOverflowing = scrollHeight > clientHeight + setShowSeeMore(isOverflowing) + // if (!isOverflowing) { + // setIsExpanded(false) + // } + } + }, [reactContent, setIsExpanded]) + + return ( + +

+
+ {reactContent} +
+ {!isExpanded && showSeeMore && ( +
+
+ setIsExpanded(true)}> + See more + +
+ )} +
+ {/* {isExpanded && showSeeMore && ( +
setIsExpanded(false)}> + See less +
+ )} */} + + ) + }, +) diff --git a/webview-ui/src/components/settings/RequestyModelPicker.tsx b/webview-ui/src/components/settings/RequestyModelPicker.tsx deleted file mode 100644 index ac72fa01c0..0000000000 --- a/webview-ui/src/components/settings/RequestyModelPicker.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import Fuse from "fuse.js" -import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react" -import { useMount } from "react-use" -import styled from "styled-components" -import { requestyDefaultModelId } from "../../../../src/shared/api" -import { useExtensionState } from "../../context/ExtensionStateContext" -import { vscode } from "../../utils/vscode" -import { highlight } from "../history/HistoryView" -import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions" - -export interface RequestyModelPickerProps { - isPopup?: boolean -} - -const RequestyModelPicker: React.FC = ({ isPopup }) => { - const { apiConfiguration, setApiConfiguration, requestyModels } = useExtensionState() - const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId) - const [isDropdownVisible, setIsDropdownVisible] = useState(false) - const [selectedIndex, setSelectedIndex] = useState(-1) - const dropdownRef = useRef(null) - const itemRefs = useRef<(HTMLDivElement | null)[]>([]) - const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const dropdownListRef = useRef(null) - - const handleModelChange = (newModelId: string) => { - // could be setting invalid model id/undefined info but validation will catch it - setApiConfiguration({ - ...apiConfiguration, - ...{ - requestyModelId: newModelId, - requestyModelInfo: requestyModels[newModelId], - }, - }) - setSearchTerm(newModelId) - } - - const { selectedModelId, selectedModelInfo } = useMemo(() => { - return normalizeApiConfiguration(apiConfiguration) - }, [apiConfiguration]) - - useMount(() => { - vscode.postMessage({ type: "refreshRequestyModels" }) - }) - - 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 modelIds = useMemo(() => { - return Object.keys(requestyModels).sort((a, b) => a.localeCompare(b)) - }, [requestyModels]) - - 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 }[] = searchTerm - ? highlight(fuse.search(searchTerm), "model-item-highlight") - : searchableItems - return results - }, [searchableItems, searchTerm, fuse]) - - const handleKeyDown = (event: KeyboardEvent) => { - 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(() => { - return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase()) - }, [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 ( -
- -
- - - { - handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase()) - setIsDropdownVisible(true) - }} - onFocus={() => setIsDropdownVisible(true)} - onKeyDown={handleKeyDown} - style={{ - width: "100%", - zIndex: REQUESTY_MODEL_PICKER_Z_INDEX, - position: "relative", - }}> - {searchTerm && ( -
{ - handleModelChange("") - setIsDropdownVisible(true) - }} - slot="end" - style={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - height: "100%", - }} - /> - )} - - {isDropdownVisible && ( - - {modelSearchResults.map((item, index) => ( - (itemRefs.current[index] = el)} - isSelected={index === selectedIndex} - onMouseEnter={() => setSelectedIndex(index)} - onClick={() => { - handleModelChange(item.id) - setIsDropdownVisible(false) - }} - dangerouslySetInnerHTML={{ - __html: item.html, - }} - /> - ))} - - )} - -
- - {hasInfo ? ( - - ) : ( -

- <> - The extension automatically fetches the latest list of models available on{" "} - - Requesty. - - If you're unsure which model to choose, Cline works best with{" "} - handleModelChange("anthropic/claude-3-5-sonnet-latest")}> - anthropic/claude-3-5-sonnet-latest. - - -

- )} -
- ) -} - -export default RequestyModelPicker - -// Dropdown - -const DropdownWrapper = styled.div` - position: relative; - width: 100%; -` - -export const REQUESTY_MODEL_PICKER_Z_INDEX = 1_000 - -const DropdownList = styled.div` - position: absolute; - top: calc(100% - 3px); - left: 0; - width: calc(100% - 2px); - max-height: 200px; - overflow-y: auto; - background-color: var(--vscode-dropdown-background); - border: 1px solid var(--vscode-list-activeSelectionBackground); - z-index: ${REQUESTY_MODEL_PICKER_Z_INDEX - 1}; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; -` - -const DropdownItem = styled.div<{ isSelected: boolean }>` - padding: 5px 10px; - cursor: pointer; - word-break: break-all; - white-space: normal; - - background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")}; - - &:hover { - background-color: var(--vscode-list-activeSelectionBackground); - } -` diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 1af8387d8c..144a598fd0 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -18,7 +18,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { customInstructions, setCustomInstructions, openRouterModels, - requestyModels, telemetrySetting, setTelemetrySetting, } = useExtensionState() @@ -27,7 +26,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { const handleSubmit = () => { const apiValidationResult = validateApiConfiguration(apiConfiguration) - const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels, requestyModels) + const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels) setApiErrorMessage(apiValidationResult) setModelIdErrorMessage(modelIdValidationResult) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index fe3ca9e677..58e90e3e84 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -2,14 +2,7 @@ import React, { createContext, useCallback, useContext, useEffect, useState } fr import { useEvent } from "react-use" import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApprovalSettings" import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "../../../src/shared/ExtensionMessage" -import { - ApiConfiguration, - ModelInfo, - openRouterDefaultModelId, - openRouterDefaultModelInfo, - requestyDefaultModelId, - requestyDefaultModelInfo, -} from "../../../src/shared/api" +import { ApiConfiguration, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api" import { findLastIndex } from "../../../src/shared/array" import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp" import { convertTextMateToHljs } from "../utils/textMateToHljs" @@ -23,7 +16,6 @@ interface ExtensionStateContextType extends ExtensionState { showWelcome: boolean theme: any openRouterModels: Record - requestyModels: Record openAiModels: string[] mcpServers: McpServer[] mcpMarketplaceCatalog: McpMarketplaceCatalog @@ -59,9 +51,6 @@ export const ExtensionStateContextProvider: React.FC<{ const [openRouterModels, setOpenRouterModels] = useState>({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, }) - const [requestyModels, setRequestyModels] = useState>({ - [requestyDefaultModelId]: requestyDefaultModelInfo, - }) const [openAiModels, setOpenAiModels] = useState([]) const [mcpServers, setMcpServers] = useState([]) @@ -76,7 +65,6 @@ export const ExtensionStateContextProvider: React.FC<{ ? [ config.apiKey, config.openRouterApiKey, - config.requestyApiKey, config.awsRegion, config.vertexProjectId, config.openAiApiKey, @@ -122,14 +110,6 @@ export const ExtensionStateContextProvider: React.FC<{ }) break } - case "requestyModels": { - const updatedModels = message.requestyModels ?? {} - setRequestyModels({ - [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } case "openRouterModels": { const updatedModels = message.openRouterModels ?? {} setOpenRouterModels({ @@ -168,7 +148,6 @@ export const ExtensionStateContextProvider: React.FC<{ showWelcome, theme, openRouterModels, - requestyModels, openAiModels, mcpServers, mcpMarketplaceCatalog, diff --git a/webview-ui/src/main.tsx b/webview-ui/src/main.tsx index a6cefc5bd2..03e45df283 100644 --- a/webview-ui/src/main.tsx +++ b/webview-ui/src/main.tsx @@ -5,7 +5,7 @@ import "./index.css" import App from "./App.tsx" import "../../node_modules/@vscode/codicons/dist/codicon.css" -const apiKey = "phc_5WnLHpYyC30Bsb7VSJ6DzcPXZ34JSF08DJLyM7svZ15" +const apiKey = "phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K" const apiHost = "https://us.i.posthog.com" createRoot(document.getElementById("root")!).render( diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 7fffe4a0f9..a3d2e4106e 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,4 +1,4 @@ -import { ApiConfiguration, openRouterDefaultModelId, requestyDefaultModelId } from "../../../src/shared/api" +import { ApiConfiguration, openRouterDefaultModelId } from "../../../src/shared/api" import { ModelInfo } from "../../../src/shared/api" export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined { if (apiConfiguration) { @@ -91,26 +91,15 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s export function validateModelId( apiConfiguration?: ApiConfiguration, openRouterModels?: Record, - requestyModels?: Record, ): string | undefined { if (apiConfiguration) { switch (apiConfiguration.apiProvider) { case "openrouter": - const openRouterModelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default - if (!openRouterModelId) { + const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default + if (!modelId) { return "You must provide a model ID." } - if (openRouterModels && !Object.keys(openRouterModels).includes(openRouterModelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - case "requesty": - const requestyModelId = apiConfiguration.requestyModelId || requestyDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default - if (!requestyModelId) { - return "You must provide a model ID." - } - if (requestyModels && !Object.keys(requestyModels).includes(requestyModelId)) { + if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) { // even if the model list endpoint failed, extensionstatecontext will always have the default model info return "The model ID you provided is not available. Please choose a different model." }