mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Remove requesty polling; fix deepseek cost calculation changes; fix preferred language parsing
This commit is contained in:
parent
df7b458229
commit
eaa76512fc
18 changed files with 289 additions and 650 deletions
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LanguageDisplay>("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<LanguageDisplay>("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 <potentially relevant details>
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<Record<string, ModelInfo> | undefined> {
|
||||
const filePath = path.join(await this.ensureCacheDirectoryExists(), filename)
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | 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<string, ModelInfo> = {}
|
||||
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<string | undefined>,
|
||||
this.getSecret("requestyApiKey") as Promise<string | undefined>,
|
||||
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("requestyModelInfo") as Promise<ModelInfo | undefined>,
|
||||
this.getSecret("togetherApiKey") as Promise<string | undefined>,
|
||||
this.getGlobalState("togetherModelId") as Promise<string | undefined>,
|
||||
this.getSecret("qwenApiKey") as Promise<string | undefined>,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, ModelInfo>
|
||||
requestyModels?: Record<string, ModelInfo>
|
||||
openAiModels?: string[]
|
||||
mcpServers?: McpServer[]
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export interface WebviewMessage {
|
|||
| "openMention"
|
||||
| "cancelTask"
|
||||
| "refreshOpenRouterModels"
|
||||
| "refreshRequestyModels"
|
||||
| "refreshOpenAiModels"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
},
|
||||
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<any[]>([])
|
||||
|
||||
|
|
@ -635,14 +635,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
// 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
|
||||
|
|
|
|||
|
|
@ -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...">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
</VSCodeTextField>
|
||||
{!apiConfiguration?.requestyApiKey && <a href="https://app.requesty.ai/manage-api">Get API Key</a>}
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.requestyModelId || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("requestyModelId")}
|
||||
placeholder={"Enter Model ID..."}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
|
||||
models. Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -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 && (
|
||||
<>
|
||||
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
|
||||
|
|
@ -1202,7 +1216,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
|||
)}
|
||||
|
||||
{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker isPopup={isPopup} />}
|
||||
{selectedProvider === "requesty" && showModelOptions && <RequestyModelPicker isPopup={isPopup} />}
|
||||
|
||||
{modelIdErrorMessage && (
|
||||
<p
|
||||
|
|
@ -1406,12 +1419,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
|
|||
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
|
||||
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
|
||||
}
|
||||
case "requesty":
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
|
||||
selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
|
||||
}
|
||||
case "openai":
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import Fuse from "fuse.js"
|
||||
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
|
||||
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import { useMount } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { openRouterDefaultModelId } from "../../../../src/shared/api"
|
||||
|
|
@ -8,6 +9,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
|
|||
import { vscode } from "../../utils/vscode"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
|
||||
export interface OpenRouterModelPickerProps {
|
||||
isPopup?: boolean
|
||||
|
|
@ -274,3 +276,158 @@ const DropdownItem = styled.div<{ isSelected: boolean }>`
|
|||
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<HTMLDivElement>(null)
|
||||
const textRef = useRef<HTMLDivElement>(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 (
|
||||
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
|
||||
<div
|
||||
ref={textContainerRef}
|
||||
style={{
|
||||
overflowY: isExpanded ? "auto" : "hidden",
|
||||
position: "relative",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
<div
|
||||
ref={textRef}
|
||||
style={{
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: isExpanded ? "unset" : 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
// whiteSpace: "pre-wrap",
|
||||
// wordBreak: "break-word",
|
||||
// overflowWrap: "anywhere",
|
||||
}}>
|
||||
{reactContent}
|
||||
</div>
|
||||
{!isExpanded && showSeeMore && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: 30,
|
||||
height: "1.2em",
|
||||
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
|
||||
}}
|
||||
/>
|
||||
<VSCodeLink
|
||||
style={{
|
||||
// cursor: "pointer",
|
||||
// color: "var(--vscode-textLink-foreground)",
|
||||
fontSize: "inherit",
|
||||
paddingRight: 0,
|
||||
paddingLeft: 3,
|
||||
backgroundColor: isPopup ? CODE_BLOCK_BG_COLOR : "var(--vscode-sideBar-background)",
|
||||
}}
|
||||
onClick={() => setIsExpanded(true)}>
|
||||
See more
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* {isExpanded && showSeeMore && (
|
||||
<div
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
color: "var(--vscode-textLink-foreground)",
|
||||
marginLeft: "auto",
|
||||
textAlign: "right",
|
||||
paddingRight: 2,
|
||||
}}
|
||||
onClick={() => setIsExpanded(false)}>
|
||||
See less
|
||||
</div>
|
||||
)} */}
|
||||
</StyledMarkdown>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<RequestyModelPickerProps> = ({ 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<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
const dropdownListRef = useRef<HTMLDivElement>(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<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(() => {
|
||||
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 (
|
||||
<div style={{ width: "100%" }}>
|
||||
<style>
|
||||
{`
|
||||
.model-item-highlight {
|
||||
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||
color: inherit;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<label htmlFor="model-search">
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
<DropdownWrapper ref={dropdownRef}>
|
||||
<VSCodeTextField
|
||||
id="model-search"
|
||||
placeholder="Search and select a model..."
|
||||
value={searchTerm}
|
||||
onInput={(e) => {
|
||||
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 && (
|
||||
<div
|
||||
className="input-icon-button codicon codicon-close"
|
||||
aria-label="Clear search"
|
||||
onClick={() => {
|
||||
handleModelChange("")
|
||||
setIsDropdownVisible(true)
|
||||
}}
|
||||
slot="end"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VSCodeTextField>
|
||||
{isDropdownVisible && (
|
||||
<DropdownList ref={dropdownListRef}>
|
||||
{modelSearchResults.map((item, index) => (
|
||||
<DropdownItem
|
||||
key={item.id}
|
||||
ref={(el) => (itemRefs.current[index] = el)}
|
||||
isSelected={index === selectedIndex}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
onClick={() => {
|
||||
handleModelChange(item.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.html,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownList>
|
||||
)}
|
||||
</DropdownWrapper>
|
||||
</div>
|
||||
|
||||
{hasInfo ? (
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 0,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<>
|
||||
The extension automatically fetches the latest list of models available on{" "}
|
||||
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://app.requesty.ai/router/list">
|
||||
Requesty.
|
||||
</VSCodeLink>
|
||||
If you're unsure which model to choose, Cline works best with{" "}
|
||||
<VSCodeLink
|
||||
style={{ display: "inline", fontSize: "inherit" }}
|
||||
onClick={() => handleModelChange("anthropic/claude-3-5-sonnet-latest")}>
|
||||
anthropic/claude-3-5-sonnet-latest.
|
||||
</VSCodeLink>
|
||||
</>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
`
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<string, ModelInfo>
|
||||
requestyModels: Record<string, ModelInfo>
|
||||
openAiModels: string[]
|
||||
mcpServers: McpServer[]
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
|
|
@ -59,9 +51,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
|||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
})
|
||||
|
||||
const [openAiModels, setOpenAiModels] = useState<string[]>([])
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<string, ModelInfo>,
|
||||
requestyModels?: Record<string, ModelInfo>,
|
||||
): 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."
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue