mirror of
https://github.com/cline/cline.git
synced 2026-09-08 22:13:11 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
756d8c9eb8 | ||
|
|
d88703dc2c | ||
|
|
2ef9553769 | ||
|
|
b28664e294 | ||
|
|
43779a21f0 | ||
|
|
cfa7fa09e3 | ||
|
|
3c9b270a19 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(vercel-ai-gateway): add model refresh and improve reasoning support
|
||||
@@ -49,6 +49,8 @@ service ModelsService {
|
||||
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
|
||||
// Fetches available models from AIhubmix
|
||||
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Vercel AI Gateway models
|
||||
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
|
||||
@@ -255,9 +255,10 @@ function createHandlerForProvider(
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
openRouterProviderSorting: options.openRouterProviderSorting,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
vercelAiGatewayModelId:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelId : options.actModeVercelAiGatewayModelId,
|
||||
vercelAiGatewayModelInfo:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelInfo : options.actModeVercelAiGatewayModelInfo,
|
||||
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
})
|
||||
case "litellm":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
@@ -13,9 +13,9 @@ import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { createVercelAIGatewayStream } from "../transform/vercel-ai-gateway-stream"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
interface ClineHandlerOptions extends CommonApiHandlerOptions {
|
||||
@@ -23,9 +23,8 @@ interface ClineHandlerOptions extends CommonApiHandlerOptions {
|
||||
taskId?: string
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
openRouterProviderSorting?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
vercelAiGatewayModelId?: string
|
||||
vercelAiGatewayModelInfo?: ModelInfo
|
||||
clineAccountId?: string
|
||||
geminiThinkingLevel?: string
|
||||
}
|
||||
@@ -107,14 +106,13 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
const stream = await createOpenRouterStream(
|
||||
const stream = await createVercelAIGatewayStream(
|
||||
client,
|
||||
systemPrompt,
|
||||
messages,
|
||||
this.getModel(),
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
this.options.openRouterProviderSorting,
|
||||
tools,
|
||||
this.options.geminiThinkingLevel,
|
||||
)
|
||||
@@ -167,7 +165,11 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
if (
|
||||
"reasoning" in delta &&
|
||||
delta.reasoning &&
|
||||
!shouldSkipReasoningForModel(this.options.vercelAiGatewayModelId)
|
||||
) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
|
||||
@@ -186,7 +188,7 @@ export class ClineHandler implements ApiHandler {
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
delta?.reasoning_details?.length && // exists and non-0
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
!shouldSkipReasoningForModel(this.options.vercelAiGatewayModelId)
|
||||
) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
@@ -272,11 +274,11 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.openRouterModelId
|
||||
const modelInfo = this.options.openRouterModelInfo
|
||||
const modelId = this.options.vercelAiGatewayModelId
|
||||
const modelInfo = this.options.vercelAiGatewayModelInfo
|
||||
if (modelId && modelInfo) {
|
||||
return { id: modelId, info: modelInfo }
|
||||
}
|
||||
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
|
||||
throw new Error("No Vercel AI Gateway model configured. Please select a model in settings.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
@@ -13,7 +14,9 @@ interface VercelAIGatewayHandlerOptions extends CommonApiHandlerOptions {
|
||||
vercelAiGatewayApiKey?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
geminiThinkingLevel?: string
|
||||
}
|
||||
|
||||
export class VercelAIGatewayHandler implements ApiHandler {
|
||||
@@ -58,8 +61,10 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
{ id: modelId, info: modelInfo },
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
tools,
|
||||
this.options.geminiThinkingLevel,
|
||||
)
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
@@ -67,6 +72,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -79,7 +85,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for models that don't support it (e.g., devstral, grok-4)
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
|
||||
@@ -91,7 +98,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
delta.reasoning_details.length // exists and non-0
|
||||
delta.reasoning_details.length && // exists and non-0
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -5,9 +5,11 @@ import {
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
import { getOpenAIToolParams } from "./tool-call-processor"
|
||||
|
||||
export async function createVercelAIGatewayStream(
|
||||
@@ -15,77 +17,206 @@ export async function createVercelAIGatewayStream(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: string; info: ModelInfo },
|
||||
reasoningEffort?: string,
|
||||
thinkingBudgetTokens?: number,
|
||||
tools?: OpenAITool[],
|
||||
geminiThinkingLevel?: string,
|
||||
) {
|
||||
// Convert Anthropic messages to OpenAI format
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
// remove the custom :1m suffix, to create the model id openrouter API expects
|
||||
// remove the custom :1m suffix, to create the model id the API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
// Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
|
||||
// historical tool calls may not include Gemini reasoning details, which can poison the next request.
|
||||
// Bandaid: for Gemini only, drop tool_calls that lack reasoning_details and their paired tool messages.
|
||||
if (model.id.includes("gemini")) {
|
||||
const droppedToolCallIds = new Set<string>()
|
||||
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
|
||||
|
||||
for (const msg of openAiMessages) {
|
||||
if (msg.role === "assistant") {
|
||||
const anyMsg = msg as any
|
||||
const toolCalls = anyMsg.tool_calls
|
||||
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||
const reasoningDetails = anyMsg.reasoning_details
|
||||
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
|
||||
if (!hasReasoningDetails) {
|
||||
for (const tc of toolCalls) {
|
||||
if (tc?.id) droppedToolCallIds.add(tc.id)
|
||||
}
|
||||
// Keep any textual content, but drop the tool_calls themselves.
|
||||
if (anyMsg.content) {
|
||||
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
const anyMsg = msg as any
|
||||
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sanitized.push(msg)
|
||||
}
|
||||
|
||||
openAiMessages = sanitized
|
||||
}
|
||||
|
||||
// Prompt caching for supported models
|
||||
// This handles cache_control for Claude and MiniMax models
|
||||
const isAnthropicModel = model.id.startsWith("anthropic/")
|
||||
const isMinimaxModel = model.id.startsWith("minimax/")
|
||||
|
||||
if (isAnthropicModel || isMinimaxModel) {
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Add cache_control to the last two user messages for conversation context caching
|
||||
// Add cache_control to the last two user messages
|
||||
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
|
||||
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
|
||||
lastTwoUserMessages.forEach((msg) => {
|
||||
if (typeof msg.content === "string" && msg.content.length > 0) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// Find the last text part in the message content
|
||||
const lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Configure reasoning parameters similar to OpenRouter
|
||||
let temperature: number | undefined = 0
|
||||
let reasoning: { max_tokens: number } | undefined
|
||||
|
||||
if (isAnthropicModel) {
|
||||
const budget_tokens = thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0
|
||||
if (reasoningOn) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: budget_tokens }
|
||||
}
|
||||
} else if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: thinkingBudgetTokens }
|
||||
// Model-specific max tokens
|
||||
// Not sure how the API defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
|
||||
let maxTokens: number | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.5":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet":
|
||||
case "anthropic/claude-3.5-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet-20240620":
|
||||
case "anthropic/claude-3.5-sonnet-20240620:beta":
|
||||
case "anthropic/claude-3-5-haiku":
|
||||
case "anthropic/claude-3-5-haiku:beta":
|
||||
case "anthropic/claude-3-5-haiku-20241022":
|
||||
case "anthropic/claude-3-5-haiku-20241022:beta":
|
||||
maxTokens = 8_192
|
||||
break
|
||||
}
|
||||
|
||||
// Model-specific temperature and topP settings
|
||||
let temperature: number | undefined = 0
|
||||
let topP: number | undefined
|
||||
if (
|
||||
model.id.startsWith("deepseek/deepseek-r1") ||
|
||||
model.id === "perplexity/sonar-reasoning" ||
|
||||
model.id === "qwen/qwq-32b:free" ||
|
||||
model.id === "qwen/qwq-32b"
|
||||
) {
|
||||
// Recommended values from DeepSeek
|
||||
temperature = 0.7
|
||||
topP = 0.95
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
if (model.id.startsWith("google/gemini-3.0") || model.id === "google/gemini-3.0") {
|
||||
// Recommended value from google
|
||||
temperature = 1.0
|
||||
}
|
||||
|
||||
// Reasoning/thinking budget configuration
|
||||
let reasoning: { max_tokens: number } | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.5":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
const budget_tokens = thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0
|
||||
if (reasoningOn) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: budget_tokens }
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (
|
||||
thinkingBudgetTokens &&
|
||||
model.info?.thinkingConfig &&
|
||||
thinkingBudgetTokens > 0 &&
|
||||
!(model.id.includes("gemini") && geminiThinkingLevel)
|
||||
) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: thinkingBudgetTokens }
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Skip reasoning for models that don't support it (e.g., devstral, grok-4)
|
||||
const includeReasoning = !shouldSkipReasoningForModel(model.id)
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
top_p: topP,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
include_reasoning: true,
|
||||
include_reasoning: includeReasoning,
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
: {}),
|
||||
// Claude Sonnet 1M provider routing - prefer Anthropic, fallback to Google Vertex
|
||||
...(isClaudeSonnet1m ? { providerOptions: { providers: { order: ["anthropic", "google-vertex"] } } } : {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models", getAxiosSettings())
|
||||
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models?include_mappings=true", getAxiosSettings())
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
|
||||
@@ -603,6 +603,8 @@ export class StateManager {
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
@@ -641,6 +643,8 @@ export class StateManager {
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
} = apiConfiguration
|
||||
|
||||
@@ -683,6 +687,8 @@ export class StateManager {
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
|
||||
// Act mode configuration updates
|
||||
@@ -722,6 +728,8 @@ export class StateManager {
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
|
||||
// Global state updates
|
||||
@@ -1280,6 +1288,11 @@ export class StateManager {
|
||||
this.taskStateCache["planModeAihubmixModelInfo"] || this.globalStateCache["planModeAihubmixModelInfo"],
|
||||
planModeNousResearchModelId:
|
||||
this.taskStateCache["planModeNousResearchModelId"] || this.globalStateCache["planModeNousResearchModelId"],
|
||||
planModeVercelAiGatewayModelId:
|
||||
this.taskStateCache["planModeVercelAiGatewayModelId"] || this.globalStateCache["planModeVercelAiGatewayModelId"],
|
||||
planModeVercelAiGatewayModelInfo:
|
||||
this.taskStateCache["planModeVercelAiGatewayModelInfo"] ||
|
||||
this.globalStateCache["planModeVercelAiGatewayModelInfo"],
|
||||
geminiPlanModeThinkingLevel:
|
||||
this.taskStateCache["geminiPlanModeThinkingLevel"] || this.globalStateCache["geminiPlanModeThinkingLevel"],
|
||||
|
||||
@@ -1350,6 +1363,11 @@ export class StateManager {
|
||||
this.taskStateCache["actModeAihubmixModelInfo"] || this.globalStateCache["actModeAihubmixModelInfo"],
|
||||
actModeNousResearchModelId:
|
||||
this.taskStateCache["actModeNousResearchModelId"] || this.globalStateCache["actModeNousResearchModelId"],
|
||||
actModeVercelAiGatewayModelId:
|
||||
this.taskStateCache["actModeVercelAiGatewayModelId"] || this.globalStateCache["actModeVercelAiGatewayModelId"],
|
||||
actModeVercelAiGatewayModelInfo:
|
||||
this.taskStateCache["actModeVercelAiGatewayModelInfo"] ||
|
||||
this.globalStateCache["actModeVercelAiGatewayModelInfo"],
|
||||
geminiActModeThinkingLevel:
|
||||
this.taskStateCache["geminiActModeThinkingLevel"] || this.globalStateCache["geminiActModeThinkingLevel"],
|
||||
nousResearchApiKey: this.secretsCache["nousResearchApiKey"],
|
||||
|
||||
@@ -402,6 +402,11 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelInfo"]>("planModeAihubmixModelInfo")
|
||||
const planModeNousResearchModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeNousResearchModelId"]>("planModeNousResearchModelId")
|
||||
const planModeVercelAiGatewayModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeVercelAiGatewayModelId"]>("planModeVercelAiGatewayModelId")
|
||||
const planModeVercelAiGatewayModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["planModeVercelAiGatewayModelInfo"]
|
||||
>("planModeVercelAiGatewayModelInfo")
|
||||
// Act mode configurations
|
||||
const actModeApiProvider = context.globalState.get<GlobalStateAndSettings["actModeApiProvider"]>("actModeApiProvider")
|
||||
const actModeApiModelId = context.globalState.get<GlobalStateAndSettings["actModeApiModelId"]>("actModeApiModelId")
|
||||
@@ -476,6 +481,11 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelId"]>("actModeAihubmixModelId")
|
||||
const actModeAihubmixModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelInfo"]>("actModeAihubmixModelInfo")
|
||||
const actModeVercelAiGatewayModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeVercelAiGatewayModelId"]>("actModeVercelAiGatewayModelId")
|
||||
const actModeVercelAiGatewayModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["actModeVercelAiGatewayModelInfo"]
|
||||
>("actModeVercelAiGatewayModelInfo")
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
if (planModeApiProvider) {
|
||||
@@ -611,6 +621,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
// Act mode configurations
|
||||
actModeApiProvider: actModeApiProvider || apiProvider,
|
||||
@@ -649,6 +661,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
|
||||
// Other global fields
|
||||
|
||||
+4
-1
@@ -177,7 +177,8 @@ export interface ApiHandlerOptions {
|
||||
planModeHicapModelId?: string
|
||||
planModeHicapModelInfo?: ModelInfo
|
||||
planModeNousResearchModelId?: string
|
||||
// Act mode configurations
|
||||
planModeVercelAiGatewayModelId?: string
|
||||
planModeVercelAiGatewayModelInfo?: ModelInfo
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiModelId?: string
|
||||
@@ -217,6 +218,8 @@ export interface ApiHandlerOptions {
|
||||
actModeHicapModelId?: string
|
||||
actModeHicapModelInfo?: ModelInfo
|
||||
actModeNousResearchModelId?: string
|
||||
actModeVercelAiGatewayModelId?: string
|
||||
actModeVercelAiGatewayModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions &
|
||||
|
||||
@@ -534,6 +534,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
planModeHicapModelId: config.planModeHicapModelId,
|
||||
planModeHicapModelInfo: convertModelInfoToProtoOpenRouter(config.planModeHicapModelInfo),
|
||||
planModeNousResearchModelId: config.planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId: config.planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.planModeVercelAiGatewayModelInfo),
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider: config.actModeApiProvider ? convertApiProviderToProto(config.actModeApiProvider) : undefined,
|
||||
@@ -573,6 +575,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
actModeHicapModelId: config.actModeHicapModelId,
|
||||
actModeHicapModelInfo: convertModelInfoToProtoOpenRouter(config.actModeHicapModelInfo),
|
||||
actModeNousResearchModelId: config.actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId: config.actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.actModeVercelAiGatewayModelInfo),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,6 +709,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
planModeHicapModelId: protoConfig.planModeHicapModelId,
|
||||
planModeHicapModelInfo: convertProtoToModelInfo(protoConfig.planModeHicapModelInfo),
|
||||
planModeNousResearchModelId: protoConfig.planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId: protoConfig.planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.planModeVercelAiGatewayModelInfo),
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider:
|
||||
@@ -745,5 +751,7 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
actModeHicapModelId: protoConfig.actModeHicapModelId,
|
||||
actModeHicapModelInfo: convertProtoToModelInfo(protoConfig.actModeHicapModelInfo),
|
||||
actModeNousResearchModelId: protoConfig.actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId: protoConfig.actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.actModeVercelAiGatewayModelInfo),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,8 @@ export interface Settings {
|
||||
planModeAihubmixModelId: string | undefined
|
||||
planModeAihubmixModelInfo: ModelInfo | undefined
|
||||
planModeNousResearchModelId: string | undefined
|
||||
planModeVercelAiGatewayModelId: string | undefined
|
||||
planModeVercelAiGatewayModelInfo: ModelInfo | undefined
|
||||
// Act mode configurations
|
||||
actModeApiProvider: ApiProvider
|
||||
actModeApiModelId: string | undefined
|
||||
@@ -211,6 +213,8 @@ export interface Settings {
|
||||
actModeAihubmixModelId: string | undefined
|
||||
actModeAihubmixModelInfo: ModelInfo | undefined
|
||||
actModeNousResearchModelId: string | undefined
|
||||
actModeVercelAiGatewayModelId: string | undefined
|
||||
actModeVercelAiGatewayModelInfo: ModelInfo | undefined
|
||||
|
||||
// OpenTelemetry configuration
|
||||
openTelemetryEnabled: boolean
|
||||
|
||||
@@ -265,6 +265,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
mode,
|
||||
apiConfiguration,
|
||||
openRouterModels,
|
||||
vercelAiGatewayModels,
|
||||
platform,
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
@@ -1046,7 +1047,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
// Separate the API config submission logic
|
||||
const submitApiConfig = useCallback(async () => {
|
||||
const apiValidationResult = validateApiConfiguration(mode, apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(mode, apiConfiguration, openRouterModels)
|
||||
const modelIdValidationResult = validateModelId(mode, apiConfiguration, openRouterModels, vercelAiGatewayModels)
|
||||
|
||||
if (!apiValidationResult && !modelIdValidationResult && apiConfiguration) {
|
||||
try {
|
||||
@@ -1067,7 +1068,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
console.error("Error refreshing state:", error)
|
||||
})
|
||||
}
|
||||
}, [apiConfiguration, openRouterModels])
|
||||
}, [apiConfiguration, openRouterModels, vercelAiGatewayModels])
|
||||
|
||||
const onModeToggle = useCallback(() => {
|
||||
// if (textAreaDisabled) return
|
||||
@@ -1149,9 +1150,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
// Get model display name
|
||||
const modelDisplayName = useMemo(() => {
|
||||
const { selectedProvider, selectedModelId } = normalizeApiConfiguration(apiConfiguration, mode)
|
||||
const { vsCodeLmModelSelector, togetherModelId, lmStudioModelId, ollamaModelId, liteLlmModelId, requestyModelId } =
|
||||
getModeSpecificFields(apiConfiguration, mode)
|
||||
const {
|
||||
vsCodeLmModelSelector,
|
||||
togetherModelId,
|
||||
lmStudioModelId,
|
||||
ollamaModelId,
|
||||
liteLlmModelId,
|
||||
requestyModelId,
|
||||
vercelAiGatewayModelId,
|
||||
} = getModeSpecificFields(apiConfiguration, mode)
|
||||
const unknownModel = "unknown"
|
||||
|
||||
if (!apiConfiguration) {
|
||||
return unknownModel
|
||||
}
|
||||
@@ -1172,6 +1181,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
return `${selectedProvider}:${liteLlmModelId}`
|
||||
case "requesty":
|
||||
return `${selectedProvider}:${requestyModelId}`
|
||||
case "vercel-ai-gateway":
|
||||
return `${selectedProvider}:${vercelAiGatewayModelId || selectedModelId}`
|
||||
case "anthropic":
|
||||
case "openrouter":
|
||||
default:
|
||||
|
||||
@@ -89,6 +89,7 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
const {
|
||||
apiConfiguration,
|
||||
openRouterModels,
|
||||
vercelAiGatewayModels,
|
||||
navigateToSettings,
|
||||
planActSeparateModelsSetting,
|
||||
showSettings,
|
||||
@@ -162,14 +163,19 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
// Get models for current provider
|
||||
const allModels = useMemo((): ModelItem[] => {
|
||||
if (OPENROUTER_MODEL_PROVIDERS.includes(selectedProvider)) {
|
||||
const modelIds = Object.keys(openRouterModels || {})
|
||||
// Use vercelAiGatewayModels for Vercel and Cline providers, openRouterModels for OpenRouter
|
||||
const modelsSource =
|
||||
selectedProvider === "vercel-ai-gateway" || selectedProvider === "cline"
|
||||
? vercelAiGatewayModels
|
||||
: openRouterModels
|
||||
const modelIds = Object.keys(modelsSource || {})
|
||||
const filteredIds = filterOpenRouterModelIds(modelIds, selectedProvider)
|
||||
|
||||
return filteredIds.map((id) => ({
|
||||
id,
|
||||
name: id.split("/").pop() || id,
|
||||
provider: id.split("/")[0],
|
||||
info: openRouterModels[id],
|
||||
info: modelsSource[id],
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -185,7 +191,7 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
}
|
||||
|
||||
return []
|
||||
}, [selectedProvider, openRouterModels, apiConfiguration])
|
||||
}, [selectedProvider, openRouterModels, vercelAiGatewayModels, apiConfiguration])
|
||||
|
||||
// Multi-word substring search - all words must match somewhere in id/name/provider
|
||||
const matchesSearch = useCallback((model: ModelItem, query: string): boolean => {
|
||||
@@ -266,7 +272,25 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
(modelId: string, modelInfo?: ModelInfoType) => {
|
||||
const modeToUse = isSplit ? activeEditMode : currentMode
|
||||
|
||||
if (OPENROUTER_MODEL_PROVIDERS.includes(selectedProvider)) {
|
||||
if (selectedProvider === "vercel-ai-gateway" || selectedProvider === "cline") {
|
||||
// Vercel AI Gateway and Cline use Vercel model fields
|
||||
const modelInfoToUse = modelInfo || vercelAiGatewayModels[modelId]
|
||||
handleModeFieldsChange(
|
||||
{
|
||||
vercelAiGatewayModelId: { plan: "planModeVercelAiGatewayModelId", act: "actModeVercelAiGatewayModelId" },
|
||||
vercelAiGatewayModelInfo: {
|
||||
plan: "planModeVercelAiGatewayModelInfo",
|
||||
act: "actModeVercelAiGatewayModelInfo",
|
||||
},
|
||||
},
|
||||
{
|
||||
vercelAiGatewayModelId: modelId,
|
||||
vercelAiGatewayModelInfo: modelInfoToUse,
|
||||
},
|
||||
modeToUse,
|
||||
)
|
||||
} else if (OPENROUTER_MODEL_PROVIDERS.includes(selectedProvider)) {
|
||||
// OpenRouter uses openRouter fields
|
||||
const modelInfoToUse = modelInfo || openRouterModels[modelId]
|
||||
handleModeFieldsChange(
|
||||
{
|
||||
@@ -309,6 +333,7 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
isSplit,
|
||||
activeEditMode,
|
||||
openRouterModels,
|
||||
vercelAiGatewayModels,
|
||||
onOpenChange,
|
||||
],
|
||||
)
|
||||
@@ -374,7 +399,8 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
// Determine which list the index falls into
|
||||
if (selectedIndex < featuredModels.length) {
|
||||
const model = featuredModels[selectedIndex]
|
||||
handleSelectModel(model.id, openRouterModels[model.id])
|
||||
// Featured models are for Cline provider which uses Vercel models
|
||||
handleSelectModel(model.id, vercelAiGatewayModels[model.id])
|
||||
} else {
|
||||
const model = filteredModels[selectedIndex - featuredModels.length]
|
||||
handleSelectModel(model.id, model.info)
|
||||
@@ -387,7 +413,7 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
break
|
||||
}
|
||||
},
|
||||
[filteredModels, featuredModels, selectedIndex, handleSelectModel, openRouterModels, onOpenChange],
|
||||
[filteredModels, featuredModels, selectedIndex, handleSelectModel, vercelAiGatewayModels, onOpenChange],
|
||||
)
|
||||
|
||||
// Reset selectedIndex and clear refs when search/provider changes
|
||||
@@ -664,9 +690,7 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SplitModeRow>
|
||||
) : (
|
||||
selectedModelId &&
|
||||
modelBelongsToProvider &&
|
||||
) : selectedModelId && modelBelongsToProvider ? (
|
||||
(() => {
|
||||
// Check if current model has a featured label (only for Cline provider)
|
||||
const currentFeaturedModel = isClineProvider
|
||||
@@ -695,7 +719,11 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
</CurrentModelRow>
|
||||
)
|
||||
})()
|
||||
)}
|
||||
) : !selectedModelId && selectedProvider === "vercel-ai-gateway" ? (
|
||||
<EmptyModelRow>
|
||||
<span className="text-[11px] text-description">Select a model below</span>
|
||||
</EmptyModelRow>
|
||||
) : null}
|
||||
|
||||
{/* For Cline: Show recommended models */}
|
||||
{isClineProvider &&
|
||||
@@ -703,7 +731,7 @@ const ModelPickerModal: React.FC<ModelPickerModalProps> = ({ isOpen, onOpenChang
|
||||
<ModelItemContainer
|
||||
$isSelected={index === selectedIndex}
|
||||
key={model.id}
|
||||
onClick={() => handleSelectModel(model.id, openRouterModels[model.id])}
|
||||
onClick={() => handleSelectModel(model.id, vercelAiGatewayModels[model.id])}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
ref={(el) => (itemRefs.current[index] = el)}>
|
||||
<ModelInfoRow>
|
||||
@@ -995,6 +1023,21 @@ const EmptyState = styled.div`
|
||||
color: var(--vscode-descriptionForeground);
|
||||
`
|
||||
|
||||
// Empty model row - shown when no model is selected for providers like Vercel
|
||||
const EmptyModelRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 10px;
|
||||
min-height: 28px;
|
||||
box-sizing: border-box;
|
||||
background: ${CODE_BLOCK_BG_COLOR};
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
border-bottom: 1px solid var(--vscode-editorGroup-border);
|
||||
`
|
||||
|
||||
// Current model row - highlighted, sticky at top when scrolling, clickable to close
|
||||
const CurrentModelRow = styled.div`
|
||||
display: flex;
|
||||
|
||||
@@ -86,7 +86,7 @@ export const freeModels = [
|
||||
label: "FREE",
|
||||
},
|
||||
{
|
||||
id: "kwaipilot/kat-coder-pro:free",
|
||||
id: "kwaipilot/kat-coder-pro-v1",
|
||||
description: "KwaiKAT's most advanced agentic coding model in the KAT-Coder series",
|
||||
label: "FREE",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import Fuse from "fuse.js"
|
||||
import type React from "react"
|
||||
import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useMount } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView } from "./common/ModelInfoView"
|
||||
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
|
||||
import { getModeSpecificFields } from "./utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
|
||||
|
||||
export interface VercelModelPickerProps {
|
||||
isPopup?: boolean
|
||||
currentMode: Mode
|
||||
}
|
||||
|
||||
const VercelModelPicker: React.FC<VercelModelPickerProps> = ({ isPopup, currentMode }) => {
|
||||
const { handleModeFieldsChange } = useApiConfigurationHandlers()
|
||||
const { apiConfiguration, vercelAiGatewayModels, refreshVercelAiGatewayModels } = useExtensionState()
|
||||
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
// Vercel AI Gateway uses its own model fields
|
||||
const [searchTerm, setSearchTerm] = useState(modeFields.vercelAiGatewayModelId || "")
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
setSearchTerm(newModelId)
|
||||
|
||||
// Vercel AI Gateway uses its own model fields
|
||||
handleModeFieldsChange(
|
||||
{
|
||||
vercelAiGatewayModelId: { plan: "planModeVercelAiGatewayModelId", act: "actModeVercelAiGatewayModelId" },
|
||||
vercelAiGatewayModelInfo: { plan: "planModeVercelAiGatewayModelInfo", act: "actModeVercelAiGatewayModelInfo" },
|
||||
},
|
||||
{
|
||||
vercelAiGatewayModelId: newModelId,
|
||||
vercelAiGatewayModelInfo: vercelAiGatewayModels[newModelId],
|
||||
},
|
||||
currentMode,
|
||||
)
|
||||
}
|
||||
|
||||
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
return {
|
||||
selectedModelId: modeFields.vercelAiGatewayModelId || "",
|
||||
selectedModelInfo: modeFields.vercelAiGatewayModelInfo as ModelInfo | undefined,
|
||||
}
|
||||
}, [modeFields.vercelAiGatewayModelId, modeFields.vercelAiGatewayModelInfo])
|
||||
|
||||
useMount(refreshVercelAiGatewayModels)
|
||||
|
||||
// Sync external changes when the modelId changes
|
||||
useEffect(() => {
|
||||
const currentModelId = modeFields.vercelAiGatewayModelId || ""
|
||||
setSearchTerm(currentModelId)
|
||||
}, [modeFields.vercelAiGatewayModelId])
|
||||
|
||||
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(vercelAiGatewayModels).sort((a, b) => a.localeCompare(b))
|
||||
}, [vercelAiGatewayModels])
|
||||
|
||||
const searchableItems = useMemo(() => {
|
||||
return modelIds.map((id) => ({
|
||||
id,
|
||||
html: id,
|
||||
}))
|
||||
}, [modelIds])
|
||||
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(searchableItems, {
|
||||
keys: ["html"],
|
||||
threshold: 0.6,
|
||||
shouldSort: true,
|
||||
isCaseSensitive: false,
|
||||
ignoreLocation: false,
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 1,
|
||||
})
|
||||
}, [searchableItems])
|
||||
|
||||
const modelSearchResults = useMemo(() => {
|
||||
const searchResults = searchTerm ? highlight(fuse.search(searchTerm), "model-item-highlight") : searchableItems
|
||||
|
||||
return searchResults
|
||||
}, [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)
|
||||
} else {
|
||||
handleModelChange(searchTerm)
|
||||
setIsDropdownVisible(false)
|
||||
}
|
||||
break
|
||||
case "Escape":
|
||||
setIsDropdownVisible(false)
|
||||
setSelectedIndex(-1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const hasInfo = useMemo(() => {
|
||||
try {
|
||||
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [modelIds, searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(-1)
|
||||
if (dropdownListRef.current) {
|
||||
dropdownListRef.current.scrollTop = 0
|
||||
}
|
||||
}, [searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
|
||||
itemRefs.current[selectedIndex]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
}, [selectedIndex])
|
||||
|
||||
const showBudgetSlider = useMemo(() => {
|
||||
return (
|
||||
selectedModelId?.toLowerCase().includes("claude-haiku-4.5") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-4.5-haiku") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-sonnet-4.5") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-sonnet-4") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-opus-4.1") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-opus-4") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-opus-4.5") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-3-7-sonnet") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet")
|
||||
)
|
||||
}, [selectedModelId])
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", paddingBottom: 2 }}>
|
||||
<style>
|
||||
{`
|
||||
.model-item-highlight {
|
||||
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||
color: inherit;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<label htmlFor="vercel-model-search">
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
|
||||
<DropdownWrapper ref={dropdownRef}>
|
||||
<VSCodeTextField
|
||||
id="vercel-model-search"
|
||||
onBlur={() => {
|
||||
if (searchTerm !== selectedModelId) {
|
||||
handleModelChange(searchTerm)
|
||||
}
|
||||
}}
|
||||
onFocus={() => setIsDropdownVisible(true)}
|
||||
onInput={(e) => {
|
||||
setSearchTerm((e.target as HTMLInputElement)?.value.toLowerCase() || "")
|
||||
setIsDropdownVisible(true)
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search and select a model..."
|
||||
style={{
|
||||
width: "100%",
|
||||
zIndex: VERCEL_MODEL_PICKER_Z_INDEX,
|
||||
position: "relative",
|
||||
}}
|
||||
value={searchTerm}>
|
||||
{searchTerm && (
|
||||
<div
|
||||
aria-label="Clear search"
|
||||
className="input-icon-button codicon codicon-close"
|
||||
onClick={() => {
|
||||
setSearchTerm("")
|
||||
setIsDropdownVisible(true)
|
||||
}}
|
||||
slot="end"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VSCodeTextField>
|
||||
{isDropdownVisible && (
|
||||
<DropdownList ref={dropdownListRef}>
|
||||
{modelSearchResults.length > 0 ? (
|
||||
modelSearchResults.map((item, index) => (
|
||||
<DropdownItem
|
||||
isSelected={index === selectedIndex}
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
handleModelChange(item.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
ref={(el) => (itemRefs.current[index] = el)}>
|
||||
<span dangerouslySetInnerHTML={{ __html: item.html }} />
|
||||
</DropdownItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownItem isSelected={false}>
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
{Object.keys(vercelAiGatewayModels).length === 0
|
||||
? "Loading models..."
|
||||
: "No models found"}
|
||||
</span>
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownList>
|
||||
)}
|
||||
</DropdownWrapper>
|
||||
</div>
|
||||
|
||||
{hasInfo && selectedModelInfo ? (
|
||||
<>
|
||||
{showBudgetSlider && <ThinkingBudgetSlider currentMode={currentMode} />}
|
||||
|
||||
<ModelInfoView
|
||||
isPopup={isPopup}
|
||||
modelInfo={selectedModelInfo}
|
||||
selectedModelId={selectedModelId}
|
||||
showProviderRouting={false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 0,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
{Object.keys(vercelAiGatewayModels).length === 0 ? (
|
||||
<>
|
||||
Enter your Vercel AI Gateway API key above to load available models. You can get an API key from{" "}
|
||||
<VSCodeLink
|
||||
href="https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
Vercel AI Gateway.
|
||||
</VSCodeLink>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Select a model from the dropdown above. The extension fetches available models from your Vercel AI
|
||||
Gateway configuration.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VercelModelPicker
|
||||
|
||||
// Dropdown styles
|
||||
|
||||
const DropdownWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
export const VERCEL_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: ${VERCEL_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);
|
||||
}
|
||||
`
|
||||
@@ -2,8 +2,8 @@ import { Mode } from "@shared/storage/types"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { DebouncedTextField } from "../common/DebouncedTextField"
|
||||
import OpenRouterModelPicker from "../OpenRouterModelPicker"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
import VercelModelPicker from "../VercelModelPicker"
|
||||
|
||||
/**
|
||||
* Props for the VercelAIGatewayProvider component
|
||||
@@ -53,11 +53,7 @@ export const VercelAIGatewayProvider = ({ showModelOptions, isPopup, currentMode
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<OpenRouterModelPicker currentMode={currentMode} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
{showModelOptions && <VercelModelPicker currentMode={currentMode} isPopup={isPopup} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,18 +258,19 @@ export function normalizeApiConfiguration(
|
||||
selectedModelInfo: requestyModelInfo || requestyDefaultModelInfo,
|
||||
}
|
||||
case "cline":
|
||||
const clineOpenRouterModelId =
|
||||
(currentMode === "plan"
|
||||
? apiConfiguration?.planModeOpenRouterModelId
|
||||
: apiConfiguration?.actModeOpenRouterModelId) || openRouterDefaultModelId
|
||||
const clineOpenRouterModelInfo =
|
||||
(currentMode === "plan"
|
||||
? apiConfiguration?.planModeOpenRouterModelInfo
|
||||
: apiConfiguration?.actModeOpenRouterModelInfo) || openRouterDefaultModelInfo
|
||||
// Cline uses Vercel AI Gateway model fields
|
||||
const clineVercelModelId =
|
||||
currentMode === "plan"
|
||||
? apiConfiguration?.planModeVercelAiGatewayModelId
|
||||
: apiConfiguration?.actModeVercelAiGatewayModelId
|
||||
const clineVercelModelInfo =
|
||||
currentMode === "plan"
|
||||
? apiConfiguration?.planModeVercelAiGatewayModelInfo
|
||||
: apiConfiguration?.actModeVercelAiGatewayModelInfo
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: clineOpenRouterModelId,
|
||||
selectedModelInfo: clineOpenRouterModelInfo,
|
||||
selectedModelId: clineVercelModelId || "",
|
||||
selectedModelInfo: clineVercelModelInfo || openRouterDefaultModelInfo,
|
||||
}
|
||||
case "openai":
|
||||
const openAiModelId =
|
||||
@@ -416,16 +417,18 @@ export function normalizeApiConfiguration(
|
||||
},
|
||||
}
|
||||
case "vercel-ai-gateway":
|
||||
// Vercel AI Gateway uses OpenRouter model fields
|
||||
// Vercel AI Gateway uses its own model fields
|
||||
const vercelModelId =
|
||||
currentMode === "plan" ? apiConfiguration?.planModeOpenRouterModelId : apiConfiguration?.actModeOpenRouterModelId
|
||||
currentMode === "plan"
|
||||
? apiConfiguration?.planModeVercelAiGatewayModelId
|
||||
: apiConfiguration?.actModeVercelAiGatewayModelId
|
||||
const vercelModelInfo =
|
||||
currentMode === "plan"
|
||||
? apiConfiguration?.planModeOpenRouterModelInfo
|
||||
: apiConfiguration?.actModeOpenRouterModelInfo
|
||||
? apiConfiguration?.planModeVercelAiGatewayModelInfo
|
||||
: apiConfiguration?.actModeVercelAiGatewayModelInfo
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: vercelModelId || openRouterDefaultModelId,
|
||||
selectedModelId: vercelModelId || "",
|
||||
selectedModelInfo: vercelModelInfo || openRouterDefaultModelInfo,
|
||||
}
|
||||
case "zai":
|
||||
@@ -512,6 +515,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
hicapModelId: undefined,
|
||||
aihubmixModelId: undefined,
|
||||
nousResearchModelId: undefined,
|
||||
vercelAiGatewayModelId: undefined,
|
||||
|
||||
// Model info objects
|
||||
openAiModelInfo: undefined,
|
||||
@@ -563,6 +567,8 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
aihubmixModelId: mode === "plan" ? apiConfiguration.planModeAihubmixModelId : apiConfiguration.actModeAihubmixModelId,
|
||||
nousResearchModelId:
|
||||
mode === "plan" ? apiConfiguration.planModeNousResearchModelId : apiConfiguration.actModeNousResearchModelId,
|
||||
vercelAiGatewayModelId:
|
||||
mode === "plan" ? apiConfiguration.planModeVercelAiGatewayModelId : apiConfiguration.actModeVercelAiGatewayModelId,
|
||||
|
||||
// Model info objects
|
||||
openAiModelInfo: mode === "plan" ? apiConfiguration.planModeOpenAiModelInfo : apiConfiguration.actModeOpenAiModelInfo,
|
||||
@@ -580,6 +586,10 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
hicapModelInfo: mode === "plan" ? apiConfiguration.planModeHicapModelInfo : apiConfiguration.actModeHicapModelInfo,
|
||||
aihubmixModelInfo:
|
||||
mode === "plan" ? apiConfiguration.planModeAihubmixModelInfo : apiConfiguration.actModeAihubmixModelInfo,
|
||||
vercelAiGatewayModelInfo:
|
||||
mode === "plan"
|
||||
? apiConfiguration.planModeVercelAiGatewayModelInfo
|
||||
: apiConfiguration.actModeVercelAiGatewayModelInfo,
|
||||
|
||||
// AWS Bedrock fields
|
||||
awsBedrockCustomSelected:
|
||||
@@ -640,13 +650,20 @@ export async function syncModeConfigurations(
|
||||
// Handle provider-specific fields
|
||||
switch (apiProvider) {
|
||||
case "openrouter":
|
||||
case "cline":
|
||||
updates.planModeOpenRouterModelId = sourceFields.openRouterModelId
|
||||
updates.actModeOpenRouterModelId = sourceFields.openRouterModelId
|
||||
updates.planModeOpenRouterModelInfo = sourceFields.openRouterModelInfo
|
||||
updates.actModeOpenRouterModelInfo = sourceFields.openRouterModelInfo
|
||||
break
|
||||
|
||||
case "cline":
|
||||
// Cline uses Vercel AI Gateway model fields
|
||||
updates.planModeVercelAiGatewayModelId = sourceFields.vercelAiGatewayModelId
|
||||
updates.actModeVercelAiGatewayModelId = sourceFields.vercelAiGatewayModelId
|
||||
updates.planModeVercelAiGatewayModelInfo = sourceFields.vercelAiGatewayModelInfo
|
||||
updates.actModeVercelAiGatewayModelInfo = sourceFields.vercelAiGatewayModelInfo
|
||||
break
|
||||
|
||||
case "requesty":
|
||||
updates.planModeRequestyModelId = sourceFields.requestyModelId
|
||||
updates.actModeRequestyModelId = sourceFields.requestyModelId
|
||||
@@ -742,11 +759,11 @@ export async function syncModeConfigurations(
|
||||
break
|
||||
|
||||
case "vercel-ai-gateway":
|
||||
// Vercel AI Gateway uses OpenRouter model fields
|
||||
updates.planModeOpenRouterModelId = sourceFields.openRouterModelId
|
||||
updates.actModeOpenRouterModelId = sourceFields.openRouterModelId
|
||||
updates.planModeOpenRouterModelInfo = sourceFields.openRouterModelInfo
|
||||
updates.actModeOpenRouterModelInfo = sourceFields.openRouterModelInfo
|
||||
// Vercel AI Gateway uses its own model fields
|
||||
updates.planModeVercelAiGatewayModelId = sourceFields.vercelAiGatewayModelId
|
||||
updates.actModeVercelAiGatewayModelId = sourceFields.vercelAiGatewayModelId
|
||||
updates.planModeVercelAiGatewayModelInfo = sourceFields.vercelAiGatewayModelInfo
|
||||
updates.actModeVercelAiGatewayModelInfo = sourceFields.vercelAiGatewayModelInfo
|
||||
break
|
||||
case "oca":
|
||||
updates.planModeOcaModelId = sourceFields.ocaModelId
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
showWelcome: boolean
|
||||
onboardingModels: OnboardingModelGroup | undefined
|
||||
openRouterModels: Record<string, ModelInfo>
|
||||
vercelAiGatewayModels: Record<string, ModelInfo>
|
||||
hicapModels: Record<string, ModelInfo>
|
||||
liteLlmModels: Record<string, ModelInfo>
|
||||
openAiModels: string[]
|
||||
@@ -86,6 +87,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
|
||||
// Refresh functions
|
||||
refreshOpenRouterModels: () => void
|
||||
refreshVercelAiGatewayModels: () => void
|
||||
refreshHicapModels: () => void
|
||||
refreshLiteLlmModels: () => void
|
||||
setUserInfo: (userInfo?: UserInfo) => void
|
||||
@@ -261,6 +263,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
const [vercelAiGatewayModels, setVercelAiGatewayModels] = useState<Record<string, ModelInfo>>({})
|
||||
const [hicapModels, setHicapModels] = useState<Record<string, ModelInfo>>({})
|
||||
const [liteLlmModels, setLiteLlmModels] = useState<Record<string, ModelInfo>>({})
|
||||
const [totalTasksSize, setTotalTasksSize] = useState<number | null>(null)
|
||||
@@ -707,15 +710,27 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
.catch((err) => console.error("Failed to refresh Baseten models:", err))
|
||||
}, [])
|
||||
|
||||
const refreshVercelAiGatewayModels = useCallback(() => {
|
||||
ModelsServiceClient.refreshVercelAiGatewayModelsRpc(EmptyRequest.create({}))
|
||||
.then((response: OpenRouterCompatibleModelInfo) => {
|
||||
const models = fromProtobufModels(response.models)
|
||||
setVercelAiGatewayModels(models)
|
||||
})
|
||||
.catch((error: Error) => console.error("Failed to refresh Vercel AI Gateway models:", error))
|
||||
}, [])
|
||||
|
||||
// Auto-refresh model lists on API key availability
|
||||
useEffect(() => {
|
||||
if (!openRouterModels || Object.keys(openRouterModels).length <= 1) {
|
||||
refreshOpenRouterModels()
|
||||
}
|
||||
if (!vercelAiGatewayModels || Object.keys(vercelAiGatewayModels).length === 0) {
|
||||
refreshVercelAiGatewayModels()
|
||||
}
|
||||
if (state.apiConfiguration?.basetenApiKey) {
|
||||
refreshBasetenModels()
|
||||
}
|
||||
}, [refreshOpenRouterModels, state?.apiConfiguration?.basetenApiKey, refreshBasetenModels])
|
||||
}, [refreshOpenRouterModels, refreshVercelAiGatewayModels, state?.apiConfiguration?.basetenApiKey, refreshBasetenModels])
|
||||
|
||||
const contextValue: ExtensionStateContextType = {
|
||||
...state,
|
||||
@@ -723,6 +738,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
showWelcome,
|
||||
onboardingModels,
|
||||
openRouterModels,
|
||||
vercelAiGatewayModels,
|
||||
hicapModels,
|
||||
liteLlmModels,
|
||||
openAiModels,
|
||||
@@ -832,6 +848,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
setMcpTab,
|
||||
setTotalTasksSize,
|
||||
refreshOpenRouterModels,
|
||||
refreshVercelAiGatewayModels,
|
||||
refreshHicapModels,
|
||||
refreshLiteLlmModels,
|
||||
onRelinquishControl,
|
||||
|
||||
@@ -177,24 +177,34 @@ export function validateModelId(
|
||||
currentMode: Mode,
|
||||
apiConfiguration?: ApiConfiguration,
|
||||
openRouterModels?: Record<string, ModelInfo>,
|
||||
vercelAiGatewayModels?: Record<string, ModelInfo>,
|
||||
): string | undefined {
|
||||
if (apiConfiguration) {
|
||||
const { apiProvider, openRouterModelId } = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
const { apiProvider, openRouterModelId, vercelAiGatewayModelId } = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
switch (apiProvider) {
|
||||
case "openrouter":
|
||||
case "cline":
|
||||
const modelId = openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
||||
if (!modelId) {
|
||||
const orModelId = openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
||||
if (!orModelId) {
|
||||
return "You must provide a model ID."
|
||||
}
|
||||
if (modelId.startsWith("@preset/")) {
|
||||
if (orModelId.startsWith("@preset/")) {
|
||||
break
|
||||
}
|
||||
if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) {
|
||||
if (openRouterModels && !Object.keys(openRouterModels).includes(orModelId)) {
|
||||
// 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 "cline":
|
||||
// Cline uses Vercel AI Gateway models
|
||||
const clineModelId = vercelAiGatewayModelId
|
||||
if (!clineModelId) {
|
||||
return "You must select a model."
|
||||
}
|
||||
if (vercelAiGatewayModels && !Object.keys(vercelAiGatewayModels).includes(clineModelId)) {
|
||||
return "The model ID you provided is not available. Please choose a different model."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
Reference in New Issue
Block a user