Compare commits

...

17 Commits

Author SHA1 Message Date
abeatrix 4532f3f750 Format generated models.dev catalog output 2026-06-29 19:20:50 -07:00
abeatrix dcf4b9ad1e Preserve provider picker options from generated catalog 2026-06-29 19:00:02 -07:00
abeatrix 76d4ea9964 Fix models.dev live catalog normalization 2026-06-29 18:47:36 -07:00
abeatrix f7d2890d5a Use models.dev catalog in legacy extension 2026-06-29 18:29:34 -07:00
Saoud Rizwan 1cbfc7197e chore(vscode): bump to 4.0.4 for legacy release 2026-06-29 13:15:03 -07:00
Tomás Barreiro 4d8f9ea1a3 Remove feature flag from the extension (#11987)
* Remove feature flag from the extension

* fix(vscode): preserve ClinePass provider metadata

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-29 13:07:42 -07:00
Saoud Rizwan 40b0b0c236 chore(vscode): bump to 4.0.3 for legacy release 2026-06-29 10:20:41 -07:00
Tomás Barreiro 8c5e1289bf Remove handler flag (#11978) 2026-06-29 10:15:45 -07:00
Saoud Rizwan 4cddde58ed chore(vscode): bump to 4.0.2 for legacy release 2026-06-28 20:54:04 -07:00
Saoud Rizwan 9d45accc3b fix: default focus chain settings in webview state (#11960)
* fix: default focus chain settings in webview state

* fix: default missing global settings from state manager

* Revert "fix: default missing global settings from state manager"

This reverts commit 76a628689e.
2026-06-28 20:52:52 -07:00
Saoud Rizwan 57760c1e20 fix: polish ClinePass and Z AI model metadata (#11958) 2026-06-28 20:11:11 -07:00
Tomás Barreiro fc33e8dbd9 Fix webview env replacing (#11955) 2026-06-28 18:37:02 -07:00
Saoud Rizwan b1dcaf576a fix(vscode): align ClinePass model resolution and reasoning controls (#11954)
* fix(vscode): show reasoning effort for ClinePass models

* fix(vscode): centralize reasoning effort support

* fix(vscode): normalize Cline recommended Z.ai ids

* fix(vscode): preserve Claude budget reasoning include flag

* fix(vscode): simplify ClinePass reasoning support

* fix(vscode): simplify reasoning effort model detection

* chore(vscode): remove unused ClinePass helper

* chore(vscode): avoid mutating ClinePass model info
2026-06-28 18:17:15 -07:00
Saoud Rizwan 419f2cea73 feat(vscode): improve ClinePass provider UX (#11950)
* feat(vscode): improve ClinePass provider UX

* fix(vscode): handle ClinePass action fallback
2026-06-28 16:33:44 -07:00
Saoud Rizwan 54d8c695a3 fix(vscode): prefer canonical Cline Z.ai model ids (#11951)
* fix(vscode): prefer canonical Cline Z.ai model ids

* fix(vscode): avoid redundant Cline model alias filtering
2026-06-28 16:13:04 -07:00
Saoud Rizwan 5a62ab8564 feat(deepseek): support reasoning effort (incl. xhigh) for thinking models (#11938)
* feat(deepseek): support reasoning effort (incl. xhigh) for thinking models

* fix(deepseek): don't forward 'none' reasoning effort to API

* fix(deepseek): scope reasoning effort selector
2026-06-28 10:55:26 -07:00
Saoud Rizwan f81afb51b5 chore(vscode): bump to 4.0.1 for legacy rollback release 2026-06-27 19:09:05 -07:00
57 changed files with 20923 additions and 2468 deletions
+32
View File
@@ -1,5 +1,37 @@
# Changelog
## [4.0.4]
### Changed
- Fully remove the ClinePass feature flag so ClinePass is available everywhere in the UI — onboarding, settings, the welcome promo banner, and the credit-limit "Switch to ClinePass" action.
## [4.0.3]
### Changed
- Enable the ClinePass provider for all users by removing the feature-flag gate that previously fell back to the standard Cline provider.
## [4.0.2]
### Added
- Add reasoning effort support (including `xhigh`) for DeepSeek thinking models.
- Improve the ClinePass provider experience with clearer reasoning controls and model selection.
### Fixed
- Show reasoning effort controls for ClinePass models and align ClinePass model resolution with the rest of the provider.
- Prefer canonical Cline Z.ai model ids and polish ClinePass and Z.ai model metadata.
- Fix environment variable replacement in the webview.
- Default focus chain settings in webview state so the toggle reflects the correct value on load.
## [4.0.1]
### Changed
- Roll the stable VS Code extension back to the pre-SDK-migration codebase to resolve regressions reported in 4.0.0. This release ships the 3.89.2 extension code under a higher version number so existing 4.0.0 users receive the update. SDK-migration work continues separately on `main`.
## [3.89.2]
### Fixed
+2 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.89.2",
"version": "4.0.4",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -387,6 +387,7 @@
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
"protos": "node scripts/build-proto.mjs",
"models": "node scripts/generate-models-dev-catalog.mjs",
"protos-python": "node scripts/build-python-proto.mjs",
"download-ripgrep": "node scripts/download-ripgrep.mjs",
"postprotos": "biome format --config-path ./biome.jsonc src/shared/proto src/generated webview-ui/src/services/grpc-client.ts --write --no-errors-on-unmatched",
@@ -0,0 +1,212 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(__dirname, "../../..");
const sdkCatalogPath = path.join(
repoRoot,
"sdk/packages/llms/src/catalog/catalog.generated.ts",
);
const outputPath = path.join(
__dirname,
"../src/shared/models/models-dev-catalog.generated.ts",
);
const { GENERATED_PROVIDER_MODELS } = await import(
pathToFileURL(sdkCatalogPath).href
);
const providerLabels = Object.fromEntries([
["anthropic", "Anthropic"],
["bedrock", "Amazon Bedrock"],
["vertex", "GCP Vertex AI"],
["gemini", "Google Gemini"],
["openai-native", "OpenAI"],
["openai-codex", "ChatGPT Subscription"],
["deepseek", "DeepSeek"],
["xai", "xAI"],
["together", "Together"],
["sapaicore", "SAP AI Core"],
["fireworks", "Fireworks AI"],
["groq", "Groq"],
["cerebras", "Cerebras"],
["sambanova", "SambaNova"],
["nebius", "Nebius AI Studio"],
["huggingface", "Hugging Face"],
["openrouter", "OpenRouter"],
["vercel-ai-gateway", "Vercel AI Gateway"],
["aihubmix", "AIhubmix"],
["baseten", "Baseten"],
["zai", "Z AI"],
["lmstudio", "LM Studio"],
["requesty", "Requesty"],
["moonshot", "Moonshot"],
["minimax", "MiniMax"],
["wandb", "W&B Inference by CoreWeave"],
["mistral", "Mistral"],
["doubao", "Bytedance Doubao"],
["qwen", "Alibaba Qwen"],
["huawei-cloud-maas", "Huawei Cloud MaaS"],
["hicap", "Hicap"],
["nousResearch", "NousResearch"],
["openai", "OpenAI Compatible"],
["ollama", "Ollama"],
["litellm", "LiteLLM"],
["claude-code", "Claude Code"],
["qwen-code", "Qwen Code"],
["dify", "Dify.ai"],
["oca", "Oracle Code Assist"],
["vscode-lm", "GitHub Copilot"],
["cline", "Cline"],
["cline-pass", "ClinePass"],
["asksage", "AskSage"],
]);
const providerOrder = [
"cline",
"cline-pass",
"openai-codex",
"gemini",
"openai",
"anthropic",
"bedrock",
"vscode-lm",
"deepseek",
"openai-native",
"openrouter",
"ollama",
"vertex",
"litellm",
"claude-code",
"sapaicore",
"mistral",
"zai",
"groq",
"cerebras",
"vercel-ai-gateway",
"baseten",
"requesty",
"fireworks",
"together",
"qwen",
"qwen-code",
"doubao",
"lmstudio",
"moonshot",
"huggingface",
"nebius",
"asksage",
"xai",
"sambanova",
"huawei-cloud-maas",
"dify",
"oca",
"minimax",
"hicap",
"aihubmix",
"nousResearch",
"wandb",
];
function toLegacyModelInfo(model) {
const capabilities = new Set(model.capabilities ?? []);
const output = {
name: model.name,
maxTokens: model.maxTokens,
contextWindow: model.contextWindow ?? model.maxInputTokens,
supportsImages: capabilities.has("images"),
supportsPromptCache: capabilities.has("prompt-cache"),
supportsReasoning: capabilities.has("reasoning"),
inputPrice: model.pricing?.input ?? 0,
outputPrice: model.pricing?.output ?? 0,
cacheWritesPrice: model.pricing?.cacheWrite ?? 0,
cacheReadsPrice: model.pricing?.cacheRead ?? 0,
supportsTools: capabilities.has("tools"),
};
for (const key of Object.keys(output)) {
if (output[key] === undefined) {
delete output[key];
}
}
return output;
}
const providerModels = Object.fromEntries(
Object.entries(GENERATED_PROVIDER_MODELS.providers).map(
([providerId, models]) => [
providerId,
Object.fromEntries(
Object.entries(models).map(([modelId, model]) => [
modelId,
toLegacyModelInfo(model),
]),
),
],
),
);
const providerOptions = providerOrder
.filter((value) => providerLabels[value])
.map((value) => ({ value, label: providerLabels[value] }));
const file = `/**
* Auto-generated from @cline/llms models.dev catalog.
*
* Source: sdk/packages/llms/src/catalog/catalog.generated.ts
* Do not edit by hand; run apps/vscode/scripts/generate-models-dev-catalog.mjs after updating the SDK model catalog.
*/
import type { ApiProvider, ModelInfo, OpenAiCompatibleModelInfo } from "../api"
export const modelsDevProviderModels = ${JSON.stringify(providerModels, null, "\t")} as const satisfies Record<string, Record<string, ModelInfo | OpenAiCompatibleModelInfo>>
export const modelsDevProviderOptions = ${JSON.stringify(providerOptions, null, "\t")} as const satisfies ReadonlyArray<{ value: ApiProvider; label: string }>
export function getModelsDevProviderModels(provider: ApiProvider | string): Record<string, ModelInfo> {
\treturn (modelsDevProviderModels[provider as keyof typeof modelsDevProviderModels] ?? {}) as Record<string, ModelInfo>
}
export const modelsDevAnthropicModels = getModelsDevProviderModels("anthropic")
export const modelsDevBedrockModels = getModelsDevProviderModels("bedrock")
export const modelsDevCerebrasModels = getModelsDevProviderModels("cerebras")
export const modelsDevDeepSeekModels = getModelsDevProviderModels("deepseek")
export const modelsDevDoubaoModels = getModelsDevProviderModels("doubao")
export const modelsDevFireworksModels = getModelsDevProviderModels("fireworks")
export const modelsDevGeminiModels = getModelsDevProviderModels("gemini")
export const modelsDevGroqModels = getModelsDevProviderModels("groq")
export const modelsDevHuggingFaceModels = getModelsDevProviderModels("huggingface")
export const modelsDevMinimaxModels = getModelsDevProviderModels("minimax")
export const modelsDevMistralModels = getModelsDevProviderModels("mistral")
export const modelsDevMoonshotModels = getModelsDevProviderModels("moonshot")
export const modelsDevNebiusModels = getModelsDevProviderModels("nebius")
export const modelsDevNousResearchModels = getModelsDevProviderModels("nousResearch")
export const modelsDevOpenAiCodexModels = getModelsDevProviderModels("openai-codex")
export const modelsDevOpenAiNativeModels = getModelsDevProviderModels("openai-native")
export const modelsDevSambanovaModels = getModelsDevProviderModels("sambanova")
export const modelsDevSapAiCoreModels = getModelsDevProviderModels("sapaicore")
export const modelsDevVertexModels = getModelsDevProviderModels("vertex")
export const modelsDevWandbModels = getModelsDevProviderModels("wandb")
export const modelsDevXaiModels = getModelsDevProviderModels("xai")
`;
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, file);
await execFileAsync(
process.execPath,
[
require.resolve("@biomejs/biome/bin/biome"),
"format",
"--write",
outputPath,
],
{ cwd: path.resolve(__dirname, "..") },
);
+16 -8
View File
@@ -1,8 +1,13 @@
import { ApiConfiguration, clinePassDefaultModelId, ModelInfo, QwenApiRegions, resolveClinePassModelInfo } from "@shared/api"
import {
ApiConfiguration,
buildModelInfoNameMap,
clinePassDefaultModelId,
ModelInfo,
QwenApiRegions,
resolveClinePassModelInfo,
} from "@shared/api"
import { Mode } from "@shared/storage/types"
import { featureFlagsService } from "@/services/feature-flags"
import { ClineStorageMessage } from "@/shared/messages/content"
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
import { Logger } from "@/shared/services/Logger"
import { ClineTool } from "@/shared/tools"
import { AIhubmixHandler } from "./providers/aihubmix"
@@ -80,10 +85,7 @@ function createHandlerForProvider(
options: Omit<ApiConfiguration, "apiProvider">,
mode: Mode,
): ApiHandler {
const effectiveApiProvider =
apiProvider === "cline-pass" && !featureFlagsService.getBooleanFlagEnabled(FeatureFlag.CLINE_PASS) ? "cline" : apiProvider
switch (effectiveApiProvider) {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler({
onRetryAttempt: options.onRetryAttempt,
@@ -204,6 +206,7 @@ function createHandlerForProvider(
onRetryAttempt: options.onRetryAttempt,
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
})
case "requesty":
return new RequestyHandler({
@@ -292,7 +295,12 @@ function createHandlerForProvider(
const clineModelId = configuredClinePassModelId?.startsWith("cline-pass/")
? configuredClinePassModelId
: clinePassDefaultModelId
const clineModelInfo = configuredClinePassModelInfo || resolveClinePassModelInfo(clineModelId)
const clineModelInfo = resolveClinePassModelInfo(
clineModelId,
configuredClinePassModelInfo
? buildModelInfoNameMap({ [clineModelId]: configuredClinePassModelInfo })
: undefined,
)
return new ClineHandler({
onRetryAttempt: options.onRetryAttempt,
clineAccountId: options.clineAccountId,
+10 -1
View File
@@ -1,7 +1,10 @@
import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import type {
ChatCompletionReasoningEffort,
ChatCompletionTool as OpenAITool,
} from "openai/resources/chat/completions"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
@@ -15,6 +18,7 @@ import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-p
interface DeepSeekHandlerOptions extends CommonApiHandlerOptions {
deepSeekApiKey?: string
apiModelId?: string
reasoningEffort?: string
}
export class DeepSeekHandler implements ApiHandler {
@@ -98,6 +102,11 @@ export class DeepSeekHandler implements ApiHandler {
stream_options: { include_usage: true },
// Only set temperature for non-thinking models
...(isDeepSeekThinkingModel ? {} : { temperature: 0 }),
// DeepSeek thinking models accept reasoning effort (low/medium map to high, xhigh maps to max).
// "none" isn't a valid DeepSeek value, so omit it and let the API use its default.
...(isDeepSeekThinkingModel && this.options.reasoningEffort && this.options.reasoningEffort !== "none"
? { reasoning_effort: this.options.reasoningEffort as ChatCompletionReasoningEffort }
: {}),
...getOpenAIToolParams(tools),
})
@@ -114,4 +114,87 @@ describe("createOpenRouterStream", () => {
should(payload.temperature).equal(undefined)
should(payload.top_p).equal(undefined)
})
it("includes reasoning for Claude budget-based reasoning models", async () => {
const { client, create } = createClient()
await createOpenRouterStream(
client as any,
"system prompt",
[{ role: "user", content: "hello" }] as any,
{
id: "anthropic/claude-sonnet-4.5",
info: createModelInfo(64_000),
},
undefined,
16_384,
)
const payload = create.firstCall.args[0] as any
payload.should.have.property("include_reasoning", true)
payload.reasoning.should.deepEqual({ max_tokens: 16_384 })
should(payload.temperature).equal(undefined)
})
it("sends reasoning effort instead of token budgets for supported OpenRouter/Cline model families", async () => {
for (const modelId of [
"zai/glm-5.2",
"z-ai/glm-5.2",
"moonshotai/kimi-k2-thinking",
"accounts/fireworks/models/minimax-m3",
"provider/mimo-vl",
"qwen/qwen3.7-max",
"deepseek/deepseek-r1",
]) {
const { client, create } = createClient()
await createOpenRouterStream(
client as any,
"system prompt",
[{ role: "user", content: "hello" }] as any,
{
id: modelId,
info: { ...createModelInfo(131_072), thinkingConfig: { maxBudget: 16_384 } },
},
"high",
16_384,
)
const payload = create.firstCall.args[0] as any
payload.should.have.property("include_reasoning", true)
payload.reasoning.should.deepEqual({ effort: "high" })
}
})
it("does not send reasoning effort for ClinePass requests when unset", async () => {
const { client, create } = createClient()
await createOpenRouterStream(client as any, "system prompt", [{ role: "user", content: "hello" }] as any, {
id: "cline-pass/glm-5.2",
info: createModelInfo(131_072),
})
const payload = create.firstCall.args[0] as any
payload.should.have.property("include_reasoning", true)
payload.should.not.have.property("reasoning")
})
it("sends the selected reasoning effort for ClinePass requests", async () => {
const { client, create } = createClient()
await createOpenRouterStream(
client as any,
"system prompt",
[{ role: "user", content: "hello" }] as any,
{
id: "cline-pass/glm-5.1",
info: createModelInfo(131_072),
},
"high",
)
const payload = create.firstCall.args[0] as any
payload.should.have.property("include_reasoning", true)
payload.reasoning.should.deepEqual({ effort: "high" })
})
})
File diff suppressed because it is too large Load Diff
@@ -135,4 +135,72 @@ describe("refreshClineModels", () => {
expect(fable1m.contextWindow).to.equal(1_000_000)
expect(fable1m.tiers).to.not.equal(undefined)
})
it("prefers Vercel-style Z.ai IDs when the Cline model list includes OpenRouter aliases", async () => {
sandbox.stub(getFeatureFlagsService(), "getBooleanFlagEnabled").callsFake((flag) => {
return flag === FeatureFlag.EXTENSION_CLINE_MODELS_ENDPOINT
})
sandbox.stub(ClineEnv, "config").returns({
environment: Environment.production,
appBaseUrl: "https://app.cline-mock.bot",
apiBaseUrl: "https://api.cline-mock.bot",
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
})
sandbox.stub(StateManager, "get").returns({
getModelsCache: () => null,
setModelsCache: () => {},
} as unknown as StateManager)
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp")
sandbox.stub(fs, "writeFile").resolves()
sandbox.stub(axios, "get").resolves({
data: {
data: [
{
id: "z-ai/glm-5.2",
name: "OpenRouter GLM 5.2",
description: "OpenRouter alias",
context_length: 128_000,
top_provider: {
max_completion_tokens: 8_192,
context_length: 128_000,
is_moderated: false,
},
architecture: {
modality: "text->text",
},
pricing: {
prompt: "0.00000098",
completion: "0.00000308",
},
supported_parameters: ["include_reasoning", "reasoning"],
},
{
id: "zai/glm-5.2",
name: "GLM 5.2",
description: "Vercel canonical ID",
context_length: 1_000_000,
top_provider: {
max_completion_tokens: 131_072,
context_length: 1_000_000,
is_moderated: false,
},
architecture: {
modality: "text->text",
},
pricing: {
prompt: "0.0000015",
completion: "0.0000045",
},
supported_parameters: ["include_reasoning", "reasoning"],
},
],
},
})
const models = await refreshClineModels({} as Controller)
expect(models["zai/glm-5.2"]).to.not.equal(undefined)
expect(models["zai/glm-5.2"].contextWindow).to.equal(1_000_000)
expect(models["z-ai/glm-5.2"]).to.equal(undefined)
})
})
@@ -127,4 +127,99 @@ describe("refreshClineRecommendedModels", () => {
expect(axiosGetStub.calledOnce).to.equal(true);
expect(secondResult).to.deep.equal(firstResult);
});
it("normalizes Cline provider Z.ai recommended IDs to the Cline API alias", async () => {
sandbox.stub(ClineEnv, "config").returns({
environment: Environment.production,
appBaseUrl: "https://app.cline-mock.bot",
apiBaseUrl: "https://api.cline-mock.bot",
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
});
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp");
sandbox.stub(fs, "writeFile").resolves();
sandbox.stub(axios, "get").resolves({
data: {
recommended: [
{
id: "zai/glm-5.2",
name: "zai/glm-5.2",
description: "Recommended GLM",
},
],
free: [
{
id: "zai/free-glm",
description: "Free GLM",
},
],
},
});
const result = await refreshClineRecommendedModels();
expect(result.recommended[0]).to.include({
id: "z-ai/glm-5.2",
name: "z-ai/glm-5.2",
});
expect(result.free[0]).to.include({
id: "z-ai/free-glm",
name: "z-ai/free-glm",
});
});
it("normalizes cached Cline provider Z.ai recommended IDs", async () => {
sandbox.stub(ClineEnv, "config").returns({
environment: Environment.production,
appBaseUrl: "https://app.cline-mock.bot",
apiBaseUrl: "https://api.cline-mock.bot",
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
});
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp");
sandbox.stub(axios, "get").rejects(new Error("network unavailable"));
sandbox.stub(fs, "access").resolves();
sandbox.stub(fs, "readFile").resolves(
JSON.stringify({
recommended: [
{
id: "zai/glm-5.2",
name: "zai/glm-5.2",
},
],
}),
);
const result = await refreshClineRecommendedModels();
expect(result.recommended.map((model) => model.id)).to.deep.equal(["z-ai/glm-5.2"]);
expect(result.recommended.map((model) => model.name)).to.deep.equal(["z-ai/glm-5.2"]);
});
it("prefers canonical ClinePass Z.ai IDs when aliases are also present", async () => {
sandbox.stub(ClineEnv, "config").returns({
environment: Environment.production,
appBaseUrl: "https://app.cline-mock.bot",
apiBaseUrl: "https://api.cline-mock.bot",
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
});
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp");
sandbox.stub(fs, "writeFile").resolves();
sandbox.stub(axios, "get").resolves({
data: {
clinePass: [
{
id: "cline-pass/z-ai/glm-5.2",
description: "OpenRouter alias",
},
{
id: "cline-pass/zai/glm-5.2",
description: "Canonical ID",
},
],
},
});
const result = await refreshClineRecommendedModels();
expect(result.clinePass.map((model) => model.id)).to.deep.equal(["cline-pass/zai/glm-5.2"]);
});
});
@@ -87,6 +87,37 @@ interface ClineRawModelInfo {
// Track pending refresh promise to prevent duplicate concurrent fetches
let pendingRefresh: Promise<Record<string, ModelInfo>> | null = null
interface ModelIdAliasRule {
canonicalPrefix: string
aliasPrefix: string
}
// Mirrors @cline/llms VERCEL_OPENROUTER_MODEL_ID_ALIAS_RULES.
const VERCEL_OPENROUTER_MODEL_ID_ALIAS_RULES = [
{ canonicalPrefix: "zai/", aliasPrefix: "z-ai/" },
] as const satisfies readonly ModelIdAliasRule[]
function preferCanonicalModelIds<T>(models: Record<string, T>, rules: readonly ModelIdAliasRule[]): Record<string, T> {
return Object.fromEntries(
Object.entries(models).filter(([modelId]) => {
for (const rule of rules) {
if (!modelId.startsWith(rule.aliasPrefix)) {
continue
}
const canonicalModelId = `${rule.canonicalPrefix}${modelId.slice(rule.aliasPrefix.length)}`
if (canonicalModelId in models) {
return false
}
}
return true
}),
)
}
function preferClineCanonicalModelIds(models: Record<string, ModelInfo>): Record<string, ModelInfo> {
return preferCanonicalModelIds(models, VERCEL_OPENROUTER_MODEL_ID_ALIAS_RULES)
}
async function fetchRawClineModels(): Promise<ClineRawModelInfo[]> {
const apiBaseUrl = ClineEnv.config().apiBaseUrl
const response = await axios.get(`${apiBaseUrl}/api/v1/ai/cline/models`, getAxiosSettings())
@@ -107,7 +138,7 @@ async function fetchRawClineModels(): Promise<ClineRawModelInfo[]> {
export async function refreshClineModels(controller: Controller): Promise<Record<string, ModelInfo>> {
const shouldUseClineEndpointSource = featureFlagsService.getBooleanFlagEnabled(FeatureFlag.EXTENSION_CLINE_MODELS_ENDPOINT)
if (!shouldUseClineEndpointSource) {
return refreshOpenRouterModels(controller)
return preferClineCanonicalModelIds(await refreshOpenRouterModels(controller))
}
// Check in-memory cache first
@@ -319,8 +350,9 @@ async function fetchAndCacheClineModels(): Promise<Record<string, ModelInfo>> {
if (Object.keys(models).length === 0) {
throw new Error("No Cline models returned from API")
}
// Save models and cache them in memory
await fs.writeFile(clineModelsFilePath, JSON.stringify(models))
await fs.writeFile(clineModelsFilePath, JSON.stringify(preferClineCanonicalModelIds(models)))
Logger.log("Cline models fetched and saved")
} catch (error) {
Logger.error("Error fetching Cline models:", error)
@@ -340,6 +372,7 @@ async function fetchAndCacheClineModels(): Promise<Record<string, ModelInfo>> {
// Avoid poisoning in-memory cache with an empty model map after transient failures.
if (Object.keys(models).length > 0) {
models = preferClineCanonicalModelIds(models)
StateManager.get().setModelsCache("cline", models)
}
@@ -23,6 +23,31 @@ export interface ClineRecommendedModelsData {
}
const RECOMMENDED_MODELS_CACHE_TTL_MS = 60 * 60 * 1000;
const CLINE_PASS_MODEL_ID_ALIAS_RULES = [
{ canonicalPrefix: "cline-pass/zai/", aliasPrefix: "cline-pass/z-ai/" },
] as const;
function normalizeClineProviderRecommendedModelId(modelId: string): string {
const zaiPrefix = "zai/";
return modelId.startsWith(zaiPrefix) ? `z-ai/${modelId.slice(zaiPrefix.length)}` : modelId;
}
function normalizeClineProviderRecommendedModels(
models: ClineRecommendedModelData[],
): ClineRecommendedModelData[] {
return models.map((model) => {
const id = normalizeClineProviderRecommendedModelId(model.id);
if (id === model.id) {
return model;
}
return {
...model,
id,
name: model.name === model.id ? id : model.name,
};
});
}
let pendingRefresh: Promise<ClineRecommendedModelsData> | null = null;
let inMemoryCache: {
@@ -30,6 +55,26 @@ let inMemoryCache: {
timestamp: number;
} | null = null;
function preferCanonicalRecommendedModels(
models: ClineRecommendedModelData[],
): ClineRecommendedModelData[] {
const modelIds = new Set(models.map((model) => model.id));
return models.filter((model) => {
for (const rule of CLINE_PASS_MODEL_ID_ALIAS_RULES) {
if (!model.id.startsWith(rule.aliasPrefix)) {
continue;
}
const canonicalModelId = `${rule.canonicalPrefix}${model.id.slice(rule.aliasPrefix.length)}`;
if (modelIds.has(canonicalModelId)) {
return false;
}
}
return true;
});
}
function normalizeRecommendedModel(
raw: unknown,
): ClineRecommendedModelData | null {
@@ -89,7 +134,11 @@ function normalizeRecommendedModelsResponse(
.map((model) => normalizeRecommendedModel(model))
.filter((model): model is ClineRecommendedModelData => model !== null);
return { recommended, free, clinePass };
return {
recommended: normalizeClineProviderRecommendedModels(recommended),
free: normalizeClineProviderRecommendedModels(free),
clinePass: preferCanonicalRecommendedModels(clinePass),
};
}
export async function refreshClineRecommendedModels(): Promise<ClineRecommendedModelsData> {
@@ -161,14 +210,9 @@ async function fetchAndCacheClineRecommendedModels(): Promise<ClineRecommendedMo
"utf8",
);
const parsed = JSON.parse(fileContents);
if (parsed) {
result = {
recommended: Array.isArray(parsed.recommended)
? parsed.recommended
: [],
free: Array.isArray(parsed.free) ? parsed.free : [],
clinePass: Array.isArray(parsed.clinePass) ? parsed.clinePass : [],
};
const normalized = normalizeRecommendedModelsResponse(parsed);
if (normalized) {
result = normalized;
Logger.log("Loaded Cline recommended models from cache");
}
}
+1 -6
View File
@@ -87,7 +87,6 @@ import {
} from "@shared/Languages";
import { USER_CONTENT_TAGS } from "@shared/messages/constants";
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message";
import { FeatureFlag } from "@shared/services/feature-flags/feature-flags";
import { type ClineDefaultTool, READ_ONLY_TOOLS } from "@shared/tools";
import type { ClineAskResponse } from "@shared/WebviewMessage";
import {
@@ -2039,11 +2038,7 @@ export class Task {
? apiConfig.planModeApiProvider
: apiConfig.actModeApiProvider
) as string;
const providerId =
configuredProviderId === "cline-pass" &&
!featureFlagsService.getBooleanFlagEnabled(FeatureFlag.CLINE_PASS)
? "cline"
: configuredProviderId;
const providerId = configuredProviderId;
const customPrompt = this.stateManager.getGlobalSettingsKey("customPrompt");
return { model, providerId, customPrompt, mode };
}
File diff suppressed because it is too large Load Diff
+254 -230
View File
@@ -1,137 +1,147 @@
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
import { WorkspaceRoot } from "@shared/multi-root/types"
import { RemoteConfigFields } from "@shared/storage/state-keys"
import type { Environment } from "../config"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { ApiConfiguration } from "./api"
import { BrowserSettings } from "./BrowserSettings"
import { ClineFeatureSetting } from "./ClineFeatureSetting"
import { BannerCardData } from "./cline/banner"
import { ClineRulesToggles } from "./cline-rules"
import { FocusChainSettings } from "./FocusChainSettings"
import { HistoryItem } from "./HistoryItem"
import { McpDisplayMode } from "./McpDisplayMode"
import { ClineMessageModelInfo } from "./messages"
import { OnboardingModelGroup } from "./proto/cline/state"
import { Mode } from "./storage/types"
import { TelemetrySetting } from "./TelemetrySetting"
import { UserInfo } from "./UserInfo"
import type { WorkspaceRoot } from "@shared/multi-root/types";
import type { RemoteConfigFields } from "@shared/storage/state-keys";
import type { Environment } from "../config";
import type { AutoApprovalSettings } from "./AutoApprovalSettings";
import type { ApiConfiguration } from "./api";
import type { BrowserSettings } from "./BrowserSettings";
import type { ClineFeatureSetting } from "./ClineFeatureSetting";
import type { BannerCardData } from "./cline/banner";
import type { ClineRulesToggles } from "./cline-rules";
import type { FocusChainSettings } from "./FocusChainSettings";
import type { HistoryItem } from "./HistoryItem";
import type { McpDisplayMode } from "./McpDisplayMode";
import type { ClineMessageModelInfo } from "./messages";
import type { ModelsDevProviderModels } from "./models/models-dev-catalog";
import type { OnboardingModelGroup } from "./proto/cline/state";
import type { Mode } from "./storage/types";
import type { TelemetrySetting } from "./TelemetrySetting";
import type { UserInfo } from "./UserInfo";
// webview will hold state
export interface ExtensionMessage {
type: "grpc_response" // New type for gRPC responses
grpc_response?: GrpcResponse
type: "grpc_response"; // New type for gRPC responses
grpc_response?: GrpcResponse;
}
export type GrpcResponse = {
message?: any // JSON serialized protobuf message
request_id: string // Same ID as the request
error?: string // Optional error message
is_streaming?: boolean // Whether this is part of a streaming response
sequence_number?: number // For ordering chunks in streaming responses
}
message?: unknown; // JSON serialized protobuf message
request_id: string; // Same ID as the request
error?: string; // Optional error message
is_streaming?: boolean; // Whether this is part of a streaming response
sequence_number?: number; // For ordering chunks in streaming responses
};
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
export type Platform =
| "aix"
| "darwin"
| "freebsd"
| "linux"
| "openbsd"
| "sunos"
| "win32"
| "unknown";
export const DEFAULT_PLATFORM = "unknown"
export const DEFAULT_PLATFORM = "unknown";
export const COMMAND_CANCEL_TOKEN = "__cline_command_cancel__"
export const COMMAND_CANCEL_TOKEN = "__cline_command_cancel__";
export interface ExtensionState {
isNewUser: boolean
welcomeViewCompleted: boolean
onboardingModels: OnboardingModelGroup | undefined
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
remoteBrowserHost?: string
preferredLanguage?: string
mode: Mode
checkpointManagerErrorMessage?: string
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
currentFocusChainChecklist?: string | null
mcpMarketplaceEnabled?: boolean
mcpDisplayMode: McpDisplayMode
planActSeparateModelsSetting: boolean
enableCheckpointsSetting?: boolean
platform: Platform
environment?: Environment
shouldShowAnnouncement: boolean
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
shellIntegrationTimeout: number
terminalReuseEnabled?: boolean
terminalOutputLineLimit: number
maxConsecutiveMistakes: number
defaultTerminalProfile?: string
vscodeTerminalExecutionMode: string
backgroundCommandRunning?: boolean
backgroundCommandTaskId?: string
lastCompletedCommandTs?: number
userInfo?: UserInfo
version: string
distinctId: string
globalClineRulesToggles: ClineRulesToggles
localClineRulesToggles: ClineRulesToggles
localWorkflowToggles: ClineRulesToggles
globalWorkflowToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
remoteRulesToggles?: ClineRulesToggles
remoteWorkflowToggles?: ClineRulesToggles
localAgentsRulesToggles: ClineRulesToggles
mcpResponsesCollapsed?: boolean
strictPlanModeEnabled?: boolean
yoloModeToggled?: boolean
useAutoCondense?: boolean
subagentsEnabled?: boolean
clineWebToolsEnabled?: ClineFeatureSetting
worktreesEnabled?: ClineFeatureSetting
focusChainSettings: FocusChainSettings
customPrompt?: string
favoritedModelIds: string[]
isNewUser: boolean;
welcomeViewCompleted: boolean;
onboardingModels: OnboardingModelGroup | undefined;
apiConfiguration?: ApiConfiguration;
autoApprovalSettings: AutoApprovalSettings;
browserSettings: BrowserSettings;
remoteBrowserHost?: string;
preferredLanguage?: string;
mode: Mode;
checkpointManagerErrorMessage?: string;
clineMessages: ClineMessage[];
currentTaskItem?: HistoryItem;
currentFocusChainChecklist?: string | null;
mcpMarketplaceEnabled?: boolean;
mcpDisplayMode: McpDisplayMode;
planActSeparateModelsSetting: boolean;
enableCheckpointsSetting?: boolean;
platform: Platform;
environment?: Environment;
shouldShowAnnouncement: boolean;
taskHistory: HistoryItem[];
telemetrySetting: TelemetrySetting;
shellIntegrationTimeout: number;
terminalReuseEnabled?: boolean;
terminalOutputLineLimit: number;
maxConsecutiveMistakes: number;
defaultTerminalProfile?: string;
vscodeTerminalExecutionMode: string;
backgroundCommandRunning?: boolean;
backgroundCommandTaskId?: string;
lastCompletedCommandTs?: number;
userInfo?: UserInfo;
version: string;
distinctId: string;
globalClineRulesToggles: ClineRulesToggles;
localClineRulesToggles: ClineRulesToggles;
localWorkflowToggles: ClineRulesToggles;
globalWorkflowToggles: ClineRulesToggles;
localCursorRulesToggles: ClineRulesToggles;
localWindsurfRulesToggles: ClineRulesToggles;
remoteRulesToggles?: ClineRulesToggles;
remoteWorkflowToggles?: ClineRulesToggles;
localAgentsRulesToggles: ClineRulesToggles;
mcpResponsesCollapsed?: boolean;
strictPlanModeEnabled?: boolean;
yoloModeToggled?: boolean;
useAutoCondense?: boolean;
subagentsEnabled?: boolean;
clineWebToolsEnabled?: ClineFeatureSetting;
worktreesEnabled?: ClineFeatureSetting;
focusChainSettings: FocusChainSettings;
customPrompt?: string;
favoritedModelIds: string[];
// NEW: Add workspace information
workspaceRoots: WorkspaceRoot[]
primaryRootIndex: number
isMultiRootWorkspace: boolean
multiRootSetting: ClineFeatureSetting
lastDismissedInfoBannerVersion: number
lastDismissedModelBannerVersion: number
lastDismissedCliBannerVersion: number
dismissedBanners?: Array<{ bannerId: string; dismissedAt: number }>
hooksEnabled?: boolean
remoteConfigSettings?: Partial<RemoteConfigFields>
globalSkillsToggles?: Record<string, boolean>
localSkillsToggles?: Record<string, boolean>
nativeToolCallSetting?: boolean
enableParallelToolCalling?: boolean
backgroundEditEnabled?: boolean
optOutOfRemoteConfig?: boolean
doubleCheckCompletionEnabled?: boolean
lazyTeammateModeEnabled?: boolean
showFeatureTips?: boolean
banners?: BannerCardData[]
welcomeBanners?: BannerCardData[]
openAiCodexIsAuthenticated?: boolean
workspaceRoots: WorkspaceRoot[];
primaryRootIndex: number;
isMultiRootWorkspace: boolean;
multiRootSetting: ClineFeatureSetting;
lastDismissedInfoBannerVersion: number;
lastDismissedModelBannerVersion: number;
lastDismissedCliBannerVersion: number;
dismissedBanners?: Array<{ bannerId: string; dismissedAt: number }>;
hooksEnabled?: boolean;
remoteConfigSettings?: Partial<RemoteConfigFields>;
globalSkillsToggles?: Record<string, boolean>;
localSkillsToggles?: Record<string, boolean>;
nativeToolCallSetting?: boolean;
enableParallelToolCalling?: boolean;
backgroundEditEnabled?: boolean;
optOutOfRemoteConfig?: boolean;
doubleCheckCompletionEnabled?: boolean;
lazyTeammateModeEnabled?: boolean;
showFeatureTips?: boolean;
banners?: BannerCardData[];
welcomeBanners?: BannerCardData[];
openAiCodexIsAuthenticated?: boolean;
modelsDevProviderModels?: ModelsDevProviderModels;
}
export interface ClineMessage {
ts: number
type: "ask" | "say"
ask?: ClineAsk
say?: ClineSay
text?: string
reasoning?: string
images?: string[]
files?: string[]
partial?: boolean
commandCompleted?: boolean
lastCheckpointHash?: string
isCheckpointCheckedOut?: boolean
isOperationOutsideWorkspace?: boolean
conversationHistoryIndex?: number
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
modelInfo?: ClineMessageModelInfo
ts: number;
type: "ask" | "say";
ask?: ClineAsk;
say?: ClineSay;
text?: string;
reasoning?: string;
images?: string[];
files?: string[];
partial?: boolean;
commandCompleted?: boolean;
lastCheckpointHash?: string;
isCheckpointCheckedOut?: boolean;
isOperationOutsideWorkspace?: boolean;
conversationHistoryIndex?: number;
conversationHistoryDeletedRange?: [number, number]; // for when conversation history is truncated for API requests
modelInfo?: ClineMessageModelInfo;
}
export type ClineAsk =
@@ -152,7 +162,7 @@ export type ClineAsk =
| "condense"
| "summarize_task"
| "report_bug"
| "use_subagents"
| "use_subagents";
export type ClineSay =
| "task"
@@ -192,7 +202,7 @@ export type ClineSay =
| "subagent"
| "use_subagents"
| "subagent_usage"
| "conditional_rules_applied"
| "conditional_rules_applied";
export interface ClineSayTool {
tool:
@@ -207,167 +217,181 @@ export interface ClineSayTool {
| "webFetch"
| "webSearch"
| "summarizeTask"
| "useSkill"
path?: string
diff?: string
content?: string
regex?: string
filePattern?: string
operationIsLocatedInWorkspace?: boolean
| "useSkill";
path?: string;
diff?: string;
content?: string;
regex?: string;
filePattern?: string;
operationIsLocatedInWorkspace?: boolean;
/** Starting line numbers in the original file where each SEARCH block matched */
startLineNumbers?: number[]
startLineNumbers?: number[];
/** Inclusive line range actually returned by read_file (for UI summaries). */
readLineStart?: number
readLineEnd?: number
readLineStart?: number;
readLineEnd?: number;
}
export interface ClineSayHook {
hookName: string // Name of the hook (e.g., "PreToolUse", "PostToolUse")
toolName?: string // Tool name if applicable (for PreToolUse/PostToolUse)
status: "running" | "completed" | "failed" | "cancelled" // Execution status
exitCode?: number // Exit code when completed
hasJsonResponse?: boolean // Whether a JSON response was parsed
hookName: string; // Name of the hook (e.g., "PreToolUse", "PostToolUse")
toolName?: string; // Tool name if applicable (for PreToolUse/PostToolUse)
status: "running" | "completed" | "failed" | "cancelled"; // Execution status
exitCode?: number; // Exit code when completed
hasJsonResponse?: boolean; // Whether a JSON response was parsed
// Pending tool information (only present during PreToolUse "running" status)
pendingToolInfo?: {
tool: string // Tool name (e.g., "write_to_file", "execute_command")
path?: string // File path for file operations
command?: string // Command for execute_command
content?: string // Content preview (first 200 chars)
diff?: string // Diff preview (first 200 chars)
regex?: string // Regex pattern for search_files
url?: string // URL for web_fetch or browser_action
mcpTool?: string // MCP tool name
mcpServer?: string // MCP server name
resourceUri?: string // MCP resource URI
}
tool: string; // Tool name (e.g., "write_to_file", "execute_command")
path?: string; // File path for file operations
command?: string; // Command for execute_command
content?: string; // Content preview (first 200 chars)
diff?: string; // Diff preview (first 200 chars)
regex?: string; // Regex pattern for search_files
url?: string; // URL for web_fetch or browser_action
mcpTool?: string; // MCP tool name
mcpServer?: string; // MCP server name
resourceUri?: string; // MCP resource URI
};
// Structured error information (only present when status is "failed")
error?: {
type: "timeout" | "validation" | "execution" | "cancellation" // Type of error
message: string // User-friendly error message
details?: string // Technical details for expansion
scriptPath?: string // Path to the hook script
}
type: "timeout" | "validation" | "execution" | "cancellation"; // Type of error
message: string; // User-friendly error message
details?: string; // Technical details for expansion
scriptPath?: string; // Path to the hook script
};
}
export type HookOutputStreamMeta = {
/** Which hook configuration the script originated from (global vs workspace). */
source: "global" | "workspace"
source: "global" | "workspace";
/** Full path to the hook script that emitted the output. */
scriptPath: string
}
scriptPath: string;
};
// must keep in sync with system prompt
export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
export type BrowserAction = (typeof browserActions)[number]
export const browserActions = [
"launch",
"click",
"type",
"scroll_down",
"scroll_up",
"close",
] as const;
export type BrowserAction = (typeof browserActions)[number];
export interface ClineSayBrowserAction {
action: BrowserAction
coordinate?: string
text?: string
action: BrowserAction;
coordinate?: string;
text?: string;
}
export interface ClineSayGenerateExplanation {
title: string
fromRef: string
toRef: string
status: "generating" | "complete" | "error"
error?: string
title: string;
fromRef: string;
toRef: string;
status: "generating" | "complete" | "error";
error?: string;
}
export type SubagentExecutionStatus = "pending" | "running" | "completed" | "failed"
export type SubagentExecutionStatus =
| "pending"
| "running"
| "completed"
| "failed";
export interface SubagentStatusItem {
index: number
prompt: string
status: SubagentExecutionStatus
toolCalls: number
inputTokens: number
outputTokens: number
totalCost: number
contextTokens: number
contextWindow: number
contextUsagePercentage: number
latestToolCall?: string
result?: string
error?: string
index: number;
prompt: string;
status: SubagentExecutionStatus;
toolCalls: number;
inputTokens: number;
outputTokens: number;
totalCost: number;
contextTokens: number;
contextWindow: number;
contextUsagePercentage: number;
latestToolCall?: string;
result?: string;
error?: string;
}
export interface ClineSaySubagentStatus {
status: "running" | "completed" | "failed"
total: number
completed: number
successes: number
failures: number
toolCalls: number
inputTokens: number
outputTokens: number
contextWindow: number
maxContextTokens: number
maxContextUsagePercentage: number
items: SubagentStatusItem[]
status: "running" | "completed" | "failed";
total: number;
completed: number;
successes: number;
failures: number;
toolCalls: number;
inputTokens: number;
outputTokens: number;
contextWindow: number;
maxContextTokens: number;
maxContextUsagePercentage: number;
items: SubagentStatusItem[];
}
export type BrowserActionResult = {
screenshot?: string
logs?: string
currentUrl?: string
currentMousePosition?: string
}
screenshot?: string;
logs?: string;
currentUrl?: string;
currentMousePosition?: string;
};
export interface ClineAskUseMcpServer {
serverName: string
type: "use_mcp_tool" | "access_mcp_resource"
toolName?: string
arguments?: string
uri?: string
serverName: string;
type: "use_mcp_tool" | "access_mcp_resource";
toolName?: string;
arguments?: string;
uri?: string;
}
export interface ClineAskUseSubagents {
prompts: string[]
prompts: string[];
}
export interface ClinePlanModeResponse {
response: string
options?: string[]
selected?: string
response: string;
options?: string[];
selected?: string;
}
export interface ClineAskQuestion {
question: string
options?: string[]
selected?: string
question: string;
options?: string[];
selected?: string;
}
export interface ClineAskNewTask {
context: string
context: string;
}
export interface ClineApiReqInfo {
request?: string
tokensIn?: number
tokensOut?: number
cacheWrites?: number
cacheReads?: number
cost?: number
cancelReason?: ClineApiReqCancelReason
streamingFailedMessage?: string
request?: string;
tokensIn?: number;
tokensOut?: number;
cacheWrites?: number;
cacheReads?: number;
cost?: number;
cancelReason?: ClineApiReqCancelReason;
streamingFailedMessage?: string;
retryStatus?: {
attempt: number
maxAttempts: number
delaySec: number
errorSnippet?: string
}
attempt: number;
maxAttempts: number;
delaySec: number;
errorSnippet?: string;
};
}
export interface ClineSubagentUsageInfo {
source: "subagents"
tokensIn: number
tokensOut: number
cacheWrites: number
cacheReads: number
cost: number
source: "subagents";
tokensIn: number;
tokensOut: number;
cacheWrites: number;
cacheReads: number;
cost: number;
}
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
export type ClineApiReqCancelReason =
| "streaming_failed"
| "user_cancelled"
| "retries_exhausted";
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES";
@@ -0,0 +1,65 @@
import { expect } from "chai"
import {
buildModelInfoNameMap,
internationalZAiModels,
mainlandZAiModels,
type ModelInfo,
resolveClinePassModelInfo,
} from "../api"
describe("ClinePass model info", () => {
const createModelInfo = (name: string, contextWindow: number): ModelInfo => ({
name,
contextWindow,
maxTokens: 8_192,
supportsPromptCache: false,
supportsReasoning: false,
thinkingConfig: { maxBudget: 16_384 },
})
it("prefers dynamic model metadata for ClinePass GLM aliases", () => {
const modelInfo = resolveClinePassModelInfo(
"cline-pass/z-ai/glm-5.2",
buildModelInfoNameMap({
"z-ai/glm-5.2": createModelInfo("OpenRouter GLM 5.2", 1_000_000),
}),
)
expect(modelInfo.name).to.equal("OpenRouter GLM 5.2")
expect(modelInfo.contextWindow).to.equal(1_000_000)
expect(modelInfo.thinkingConfig).to.deep.equal({ maxBudget: 16_384 })
})
it("falls back to static ClinePass metadata when dynamic metadata is unavailable", () => {
const modelInfo = resolveClinePassModelInfo("cline-pass/glm-5.2")
expect(modelInfo.contextWindow).to.equal(202_752)
expect(modelInfo.supportsReasoning).to.equal(true)
expect(modelInfo.thinkingConfig).to.equal(undefined)
})
it("preserves dynamic ClinePass model info when no static alias exists", () => {
const modelInfo = resolveClinePassModelInfo(
"cline-pass/new-model",
buildModelInfoNameMap({
"zai/new-model": createModelInfo("New model", 1_000_000),
}),
)
expect(modelInfo.name).to.equal("New model")
expect(modelInfo.supportsReasoning).to.equal(false)
expect(modelInfo.thinkingConfig).to.deep.equal({ maxBudget: 16_384 })
})
})
describe("Z AI model info", () => {
it("includes GLM 5.2 for both direct Z AI entrypoints", () => {
for (const models of [internationalZAiModels, mainlandZAiModels]) {
expect(models["glm-5.2"].contextWindow).to.equal(1_000_000)
expect(models["glm-5.2"].maxTokens).to.equal(128_000)
expect(models["glm-5.2"].inputPrice).to.equal(1.4)
expect(models["glm-5.2"].outputPrice).to.equal(4.4)
expect(models["glm-5.2"].cacheReadsPrice).to.equal(0.26)
}
})
})
+37 -2
View File
@@ -1025,7 +1025,7 @@ export const clineDevstralModelInfo: ModelInfo = {
}
export type ClinePassModelId = keyof typeof clinePassModels
export const clinePassDefaultModelId = "cline-pass/glm-5.1"
export const clinePassDefaultModelId = "cline-pass/glm-5.2"
export const clinePassModelInfoSaneDefaults: ModelInfo = {
maxTokens: 8_192,
contextWindow: 128_000,
@@ -1039,6 +1039,19 @@ export const clinePassModelInfoSaneDefaults: ModelInfo = {
description: "",
}
export const clinePassModels = {
"cline-pass/glm-5.2": {
name: "cline-pass/glm-5.2",
maxTokens: 131_072,
contextWindow: 202_752,
supportsImages: false,
supportsPromptCache: true,
supportsReasoning: true,
inputPrice: 0.98,
outputPrice: 3.08,
cacheReadsPrice: 0.182,
cacheWritesPrice: 0,
description: "",
},
"cline-pass/glm-5.1": {
name: "cline-pass/glm-5.1",
maxTokens: 131_072,
@@ -1069,9 +1082,12 @@ export function buildModelInfoNameMap(models: Record<string, ModelInfo>): Record
}
export function resolveClinePassModelInfo(modelId: string, modelInfoByName?: Record<string, ModelInfo>): ModelInfo {
const modelSlug = getModelSlug(modelId)
const clinePassSlugModelId = `cline-pass/${modelSlug}`
return (
modelInfoByName?.[modelSlug] ??
clinePassModels[modelId as keyof typeof clinePassModels] ??
modelInfoByName?.[getModelSlug(modelId)] ??
clinePassModels[clinePassSlugModelId as keyof typeof clinePassModels] ??
clinePassModelInfoSaneDefaults
)
}
@@ -4983,12 +4999,22 @@ export type BasetenModelId = keyof typeof basetenModels
export const basetenDefaultModelId = "zai-org/GLM-4.6" satisfies BasetenModelId
// Z AI
// https://docs.z.ai/guides/llm/glm-5.2
// https://docs.z.ai/guides/llm/glm-5.1
// https://docs.z.ai/guides/llm/glm-5
// https://docs.z.ai/guides/overview/pricing
export type internationalZAiModelId = keyof typeof internationalZAiModels
export const internationalZAiDefaultModelId: internationalZAiModelId = "glm-5.1"
export const internationalZAiModels = {
"glm-5.2": {
maxTokens: 128_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: true,
cacheReadsPrice: 0.26,
inputPrice: 1.4,
outputPrice: 4.4,
},
"glm-5.1": {
maxTokens: 128_000,
contextWindow: 200_000,
@@ -5054,6 +5080,15 @@ export const internationalZAiModels = {
export type mainlandZAiModelId = keyof typeof mainlandZAiModels
export const mainlandZAiDefaultModelId: mainlandZAiModelId = "glm-5.1"
export const mainlandZAiModels = {
"glm-5.2": {
maxTokens: 128_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: true,
cacheReadsPrice: 0.26,
inputPrice: 1.4,
outputPrice: 4.4,
},
"glm-5.1": {
maxTokens: 128_000,
contextWindow: 200_000,
@@ -0,0 +1,96 @@
import { describe, it } from "mocha";
import "should";
import { normalizeModelsDevProviderModels } from "../models-dev-catalog";
describe("models.dev catalog facade", () => {
it("normalizes live models for providers that have generated fallback exports", () => {
const providerModels = normalizeModelsDevProviderModels({
mistral: {
models: {
"mistral-new": {
name: "Mistral New",
tool_call: true,
release_date: "2026-04-01",
},
},
},
doubao: {
models: {
"doubao-new": {
name: "Doubao New",
tool_call: true,
release_date: "2026-03-01",
},
},
},
"nous-research": {
models: {
"nous-new": {
name: "Nous New",
tool_call: true,
release_date: "2026-02-01",
},
},
},
});
Object.keys(providerModels.mistral).should.deepEqual(["mistral-new"]);
Object.keys(providerModels.doubao).should.deepEqual(["doubao-new"]);
Object.keys(providerModels.nousResearch).should.deepEqual(["nous-new"]);
});
it("sorts live models by release date before falling back to model id", () => {
const providerModels = normalizeModelsDevProviderModels({
anthropic: {
models: {
"z-old": {
name: "Z Old",
tool_call: true,
release_date: "2025-01-01",
},
"a-new": {
name: "A New",
tool_call: true,
release_date: "2026-01-01",
},
"b-same-date": {
name: "B Same Date",
tool_call: true,
release_date: "2026-01-01",
},
},
},
});
Object.keys(providerModels.anthropic).should.deepEqual([
"a-new",
"b-same-date",
"z-old",
]);
});
it("excludes deprecated models and models without tool calls", () => {
const providerModels = normalizeModelsDevProviderModels({
anthropic: {
models: {
active: {
name: "Active",
tool_call: true,
status: "active",
},
deprecated: {
name: "Deprecated",
tool_call: true,
status: "deprecated",
},
"no-tools": {
name: "No Tools",
tool_call: false,
},
},
},
});
Object.keys(providerModels.anthropic).should.deepEqual(["active"]);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,290 @@
import type { ApiProvider, ModelInfo, OpenAiCompatibleModelInfo } from "../api";
import {
modelsDevProviderModels as generatedModelsDevProviderModels,
modelsDevProviderOptions,
} from "./models-dev-catalog.generated";
export { modelsDevProviderOptions };
export type ModelsDevProviderModels = Record<
string,
Record<string, ModelInfo | OpenAiCompatibleModelInfo>
>;
type ModelsDevModel = {
name?: string;
tool_call?: boolean;
reasoning?: boolean;
structured_output?: boolean;
temperature?: boolean;
release_date?: string;
family?: string;
limit?: {
context?: number;
input?: number;
output?: number;
};
cost?: {
input?: number;
output?: number;
cache_read?: number;
cache_write?: number;
};
modalities?: {
input?: string[];
};
status?: string;
};
type ModelsDevPayload = Record<
string,
{ models?: Record<string, ModelsDevModel> }
>;
const MODELS_DEV_PROVIDER_KEY_MAP: ReadonlyArray<{
source: string;
target: ApiProvider;
}> = [
{ source: "openai", target: "openai-native" },
{ source: "openai", target: "openai-codex" },
{ source: "anthropic", target: "anthropic" },
{ source: "google", target: "gemini" },
{ source: "deepseek", target: "deepseek" },
{ source: "doubao", target: "doubao" },
{ source: "xai", target: "xai" },
{ source: "mistral", target: "mistral" },
{ source: "togetherai", target: "together" },
{ source: "sap-ai-core", target: "sapaicore" },
{ source: "ollama-cloud", target: "ollama" },
{ source: "fireworks-ai", target: "fireworks" },
{ source: "groq", target: "groq" },
{ source: "cerebras", target: "cerebras" },
{ source: "sambanova", target: "sambanova" },
{ source: "nebius", target: "nebius" },
{ source: "huggingface", target: "huggingface" },
{ source: "openrouter", target: "openrouter" },
{ source: "openrouter", target: "cline" },
{ source: "nous-research", target: "nousResearch" },
{ source: "vercel", target: "vercel-ai-gateway" },
{ source: "aihubmix", target: "aihubmix" },
{ source: "baseten", target: "baseten" },
{ source: "google-vertex", target: "vertex" },
{ source: "lmstudio", target: "lmstudio" },
{ source: "zai", target: "zai" },
{ source: "requesty", target: "requesty" },
{ source: "amazon-bedrock", target: "bedrock" },
{ source: "moonshotai", target: "moonshot" },
{ source: "minimax", target: "minimax" },
{ source: "wandb", target: "wandb" },
];
const DEFAULT_MAX_INPUT_TOKENS = 4096;
const DEFAULT_MAX_TOKENS = 4096;
let liveModelsDevProviderModels: ModelsDevProviderModels | undefined;
type ModelsDevModelInfo = (ModelInfo | OpenAiCompatibleModelInfo) & {
releaseDate?: string;
};
export function setLiveModelsDevProviderModels(
models: ModelsDevProviderModels | undefined,
): void {
liveModelsDevProviderModels =
models && Object.keys(models).length > 0 ? models : undefined;
refreshNamedModelExports();
}
export function getModelsDevProviderModels(
provider: ApiProvider | string,
): Record<string, ModelInfo> {
return (liveModelsDevProviderModels?.[provider] ??
generatedModelsDevProviderModels[
provider as keyof typeof generatedModelsDevProviderModels
] ??
{}) as Record<string, ModelInfo>;
}
export let modelsDevAnthropicModels = getModelsDevProviderModels("anthropic");
export let modelsDevBedrockModels = getModelsDevProviderModels("bedrock");
export let modelsDevCerebrasModels = getModelsDevProviderModels("cerebras");
export let modelsDevDeepSeekModels = getModelsDevProviderModels("deepseek");
export let modelsDevDoubaoModels = getModelsDevProviderModels("doubao");
export let modelsDevFireworksModels = getModelsDevProviderModels("fireworks");
export let modelsDevGeminiModels = getModelsDevProviderModels("gemini");
export let modelsDevGroqModels = getModelsDevProviderModels("groq");
export let modelsDevHuggingFaceModels =
getModelsDevProviderModels("huggingface");
export let modelsDevMinimaxModels = getModelsDevProviderModels("minimax");
export let modelsDevMistralModels = getModelsDevProviderModels("mistral");
export let modelsDevMoonshotModels = getModelsDevProviderModels("moonshot");
export let modelsDevNebiusModels = getModelsDevProviderModels("nebius");
export let modelsDevNousResearchModels =
getModelsDevProviderModels("nousResearch");
export let modelsDevOpenAiCodexModels =
getModelsDevProviderModels("openai-codex");
export let modelsDevOpenAiNativeModels =
getModelsDevProviderModels("openai-native");
export let modelsDevSambanovaModels = getModelsDevProviderModels("sambanova");
export let modelsDevSapAiCoreModels = getModelsDevProviderModels("sapaicore");
export let modelsDevVertexModels = getModelsDevProviderModels("vertex");
export let modelsDevWandbModels = getModelsDevProviderModels("wandb");
export let modelsDevXaiModels = getModelsDevProviderModels("xai");
function refreshNamedModelExports(): void {
modelsDevAnthropicModels = getModelsDevProviderModels("anthropic");
modelsDevBedrockModels = getModelsDevProviderModels("bedrock");
modelsDevCerebrasModels = getModelsDevProviderModels("cerebras");
modelsDevDeepSeekModels = getModelsDevProviderModels("deepseek");
modelsDevDoubaoModels = getModelsDevProviderModels("doubao");
modelsDevFireworksModels = getModelsDevProviderModels("fireworks");
modelsDevGeminiModels = getModelsDevProviderModels("gemini");
modelsDevGroqModels = getModelsDevProviderModels("groq");
modelsDevHuggingFaceModels = getModelsDevProviderModels("huggingface");
modelsDevMinimaxModels = getModelsDevProviderModels("minimax");
modelsDevMistralModels = getModelsDevProviderModels("mistral");
modelsDevMoonshotModels = getModelsDevProviderModels("moonshot");
modelsDevNebiusModels = getModelsDevProviderModels("nebius");
modelsDevNousResearchModels = getModelsDevProviderModels("nousResearch");
modelsDevOpenAiCodexModels = getModelsDevProviderModels("openai-codex");
modelsDevOpenAiNativeModels = getModelsDevProviderModels("openai-native");
modelsDevSambanovaModels = getModelsDevProviderModels("sambanova");
modelsDevSapAiCoreModels = getModelsDevProviderModels("sapaicore");
modelsDevVertexModels = getModelsDevProviderModels("vertex");
modelsDevWandbModels = getModelsDevProviderModels("wandb");
modelsDevXaiModels = getModelsDevProviderModels("xai");
}
export async function fetchLiveModelsDevProviderModels(
url = "https://models.dev/api.json",
fetcher: typeof fetch = fetch,
): Promise<ModelsDevProviderModels> {
const response = await fetcher(url);
if (!response.ok) {
throw new Error(
`Failed to load model catalog from ${url}: HTTP ${response.status}`,
);
}
return normalizeModelsDevProviderModels(
(await response.json()) as ModelsDevPayload,
);
}
export function normalizeModelsDevProviderModels(
payload: ModelsDevPayload,
): ModelsDevProviderModels {
const providerModels: ModelsDevProviderModels = {};
for (const {
source: sourceProviderKey,
target: targetProviderId,
} of MODELS_DEV_PROVIDER_KEY_MAP) {
const source = payload[sourceProviderKey];
if (!source?.models) {
continue;
}
const models: Record<string, ModelInfo | OpenAiCompatibleModelInfo> = {};
for (const [modelId, model] of Object.entries(source.models)) {
if (model.tool_call !== true || model.status === "deprecated") {
continue;
}
models[modelId] = toModelInfo(modelId, model);
}
if (Object.keys(models).length > 0) {
providerModels[targetProviderId] = sortModelsByReleaseDate(models);
}
}
return providerModels;
}
function toModelInfo(
_modelId: string,
model: ModelsDevModel,
): ModelsDevModelInfo {
const inputLimit = resolveMaxInputTokens(model.limit);
const outputLimit = Math.floor(model.limit?.output ?? DEFAULT_MAX_TOKENS);
const capabilities = toCapabilities(model);
return {
name: model.name,
contextWindow: model.limit?.context ?? inputLimit,
maxTokens: outputLimit,
supportsImages: capabilities.has("images"),
supportsPromptCache: capabilities.has("prompt-cache"),
supportsReasoning: capabilities.has("reasoning"),
inputPrice: model.cost?.input ?? 0,
outputPrice: model.cost?.output ?? 0,
cacheReadsPrice: model.cost?.cache_read ?? 0,
cacheWritesPrice: model.cost?.cache_write ?? 0,
supportsTools: capabilities.has("tools"),
releaseDate: model.release_date,
};
}
function resolveMaxInputTokens(
limit: ModelsDevModel["limit"] | undefined,
): number {
const contextLimit = limit?.context;
const inputLimit = limit?.input;
if (typeof contextLimit === "number" && typeof inputLimit === "number") {
return Math.min(contextLimit, inputLimit);
}
return inputLimit ?? contextLimit ?? DEFAULT_MAX_INPUT_TOKENS;
}
function toCapabilities(model: ModelsDevModel): Set<string> {
const capabilities = new Set<string>();
if (model.modalities?.input?.includes("image")) {
capabilities.add("images");
}
if (model.tool_call === true) {
capabilities.add("tools");
}
if (model.reasoning === true) {
capabilities.add("reasoning");
}
if (model.structured_output === true) {
capabilities.add("structured_output");
}
if (model.temperature === true) {
capabilities.add("temperature");
}
if (
(model.cost?.cache_read !== undefined && model.cost.cache_read >= 0) ||
(model.cost?.cache_write !== undefined && model.cost.cache_write >= 0)
) {
capabilities.add("prompt-cache");
}
return capabilities;
}
function sortModelsByReleaseDate<
T extends ModelInfo | OpenAiCompatibleModelInfo,
>(models: Record<string, T>): Record<string, T> {
return Object.fromEntries(
Object.entries(models).sort(([modelIdA, modelA], [modelIdB, modelB]) => {
const releaseDateA = parseReleaseDate(
(modelA as ModelsDevModelInfo).releaseDate,
);
const releaseDateB = parseReleaseDate(
(modelB as ModelsDevModelInfo).releaseDate,
);
if (releaseDateA !== releaseDateB) {
return releaseDateB - releaseDateA;
}
return modelIdA.localeCompare(modelIdB);
}),
);
}
function parseReleaseDate(value: string | undefined): number {
if (!value) {
return Number.NEGATIVE_INFINITY;
}
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? Number.NEGATIVE_INFINITY : timestamp;
}
@@ -37,11 +37,20 @@ export function supportsReasoningEffortForModel(modelId?: string): boolean {
const id = modelId.toLowerCase()
return (
id.includes("deepseek") ||
id.includes("gemini") ||
id.includes("glm") ||
id.includes("gpt") ||
id.includes("kimi") ||
id.includes("mimo") ||
id.includes("minimax") ||
id.includes("moonshot") ||
id.startsWith("openai/o") ||
id.includes("/o") ||
id.startsWith("o") ||
id.includes("qwen") ||
id.includes("z-ai") ||
id.includes("zai") ||
id.includes("grok")
)
}
@@ -13,6 +13,7 @@ import {
isPoolsideModelFamily,
modelDoesntSupportWebp,
shouldSkipReasoningForModel,
supportsReasoningEffortForModel,
} from "../model-utils"
// Minimal helper — modelDoesntSupportWebp only reads apiHandlerModel.id
@@ -37,6 +38,7 @@ describe("shouldSkipReasoningForModel", () => {
shouldSkipReasoningForModel("claude-3-sonnet").should.equal(false)
shouldSkipReasoningForModel("gpt-4").should.equal(false)
shouldSkipReasoningForModel("gemini-pro").should.equal(false)
shouldSkipReasoningForModel("zai/glm-5.2").should.equal(false)
})
it("should return false for undefined or empty model IDs", () => {
@@ -50,6 +52,31 @@ describe("shouldSkipReasoningForModel", () => {
})
})
describe("supportsReasoningEffortForModel", () => {
it("should return true for OpenRouter/Cline model families that use reasoning effort", () => {
for (const modelId of [
"zai/glm-5.2",
"z-ai/glm-5.2",
"cline-pass/glm-5.2",
"moonshotai/kimi-k2-thinking",
"kimi-k2-thinking",
"accounts/fireworks/models/kimi-k2p6",
"accounts/fireworks/models/minimax-m3",
"minimax/MiniMax-M2.7",
"provider/mimo-vl",
"qwen/qwen3.7-max",
"deepseek/deepseek-r1",
]) {
supportsReasoningEffortForModel(modelId).should.equal(true)
}
})
it("should return false for undefined and unrelated model IDs", () => {
supportsReasoningEffortForModel(undefined).should.equal(false)
supportsReasoningEffortForModel("anthropic/claude-sonnet-4.5").should.equal(false)
})
})
describe("isClaude4PlusModelFamily", () => {
it("should return true for Claude 4+ model IDs with version numbers", () => {
isClaude4PlusModelFamily("claude-sonnet-4-5-20250929").should.equal(true)
+1 -1
View File
@@ -46,7 +46,7 @@ export function shouldSkipReasoningForModel(modelId?: string): boolean {
if (!modelId) {
return false
}
return modelId.includes("grok-4") || modelId.includes("devstral") || modelId.includes("glm")
return modelId.includes("grok-4") || modelId.includes("devstral")
}
export function isAnthropicModelId(modelId: string): modelId is AnthropicModelId {
@@ -1106,6 +1106,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
switch (selectedProvider) {
case "cline":
return `${selectedProvider}:${selectedModelId}`
case "cline-pass":
return `${selectedProvider}:${selectedModelId.replace(/^cline-pass\//, "")}`
case "openai":
return `openai-compat:${selectedModelId}`
case "vscode-lm":
@@ -3,7 +3,9 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useMemo, useState } from "react"
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
import { useClineAuth } from "@/context/ClineAuthContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AccountServiceClient, TaskServiceClient } from "@/services/grpc-client"
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
interface CreditLimitErrorProps {
currentBalance: number
@@ -26,7 +28,11 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
totalSpent,
}) => {
const { activeOrganization } = useClineAuth()
const { mode, navigateToSettings } = useExtensionState()
const { handleModeFieldChange } = useApiConfigurationHandlers()
const [fullBuyCreditsUrl, setFullBuyCreditsUrl] = useState<string>("")
const [isSwitchingToClinePass, setIsSwitchingToClinePass] = useState(false)
const [didSwitchToClinePass, setDidSwitchToClinePass] = useState(false)
const dashboardUrl = useMemo(() => {
return buyCreditsUrl ?? (activeOrganization?.organizationId ? DEFAULT_BUY_CREDITS_URL.ORG : DEFAULT_BUY_CREDITS_URL.USER)
@@ -48,6 +54,19 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
fetchCallbackUrl()
}, [dashboardUrl])
const handleSwitchToClinePass = async () => {
setIsSwitchingToClinePass(true)
try {
await handleModeFieldChange({ plan: "planModeApiProvider", act: "actModeApiProvider" }, "cline-pass", mode)
setDidSwitchToClinePass(true)
navigateToSettings("api-config")
} catch (error) {
console.error("Failed to switch to ClinePass:", error)
} finally {
setIsSwitchingToClinePass(false)
}
}
// We have to divide because the balance is stored in microcredits
return (
<div className="p-2 border-none rounded-md mb-2 bg-(--vscode-textBlockQuote-background)">
@@ -66,6 +85,24 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
</div>
</div>
<div className="mb-2">
<div className="text-(--vscode-descriptionForeground) text-xs mb-2">
Trying to use ClinePass instead of credits?
</div>
<VSCodeButton
appearance="secondary"
className="w-full"
disabled={isSwitchingToClinePass || didSwitchToClinePass}
onClick={handleSwitchToClinePass}>
<span className="codicon codicon-arrow-swap mr-1.5" />
{isSwitchingToClinePass
? "Switching..."
: didSwitchToClinePass
? "Switched to ClinePass"
: "Switch to ClinePass"}
</VSCodeButton>
</div>
<VSCodeButtonLink className="w-full mb-2" href={fullBuyCreditsUrl}>
<span className="codicon codicon-credit-card mr-[6px] text-[14px]" />
Buy Credits
@@ -2,12 +2,13 @@ import { BANNER_DATA, BannerAction, BannerActionType, BannerCardData } from "@sh
import { EmptyRequest } from "@shared/proto/cline/common"
import type { Worktree } from "@shared/proto/cline/worktree"
import { TrackWorktreeViewOpenedRequest } from "@shared/proto/cline/worktree"
import { GitBranch } from "lucide-react"
import { GitBranch, Sparkles } from "lucide-react"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import BannerCarousel from "@/components/common/BannerCarousel"
import BannerCarousel, { type BannerData } from "@/components/common/BannerCarousel"
import WhatsNewModal from "@/components/common/WhatsNewModal"
import HistoryPreview from "@/components/history/HistoryPreview"
import { useApiConfigurationHandlers } from "@/components/settings/utils/useApiConfigurationHandlers"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import HomeHeader from "@/components/welcome/HomeHeader"
import { SuggestedTasks } from "@/components/welcome/SuggestedTasks"
@@ -16,9 +17,12 @@ import { useClineAuth } from "@/context/ClineAuthContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AccountServiceClient, StateServiceClient, UiServiceClient, WorktreeServiceClient } from "@/services/grpc-client"
import { convertBannerData } from "@/utils/bannerUtils"
import { buildClinePassSubscriptionUrl } from "@/utils/clinePassSubscription"
import { getCurrentPlatform } from "@/utils/platformUtils"
import { WelcomeSectionProps } from "../../types/chatTypes"
const CLINE_PASS_PROMO_BANNER_ID = "cline-pass-home-promo-v2"
/**
* Welcome section shown when there's no active task
* Includes info banner, announcements, home header, and history preview
@@ -68,6 +72,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
welcomeBanners,
} = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const [dismissedLocalBanners, setDismissedLocalBanners] = useState<Set<string>>(() => new Set())
// Open modal once we have welcome banners
useEffect(() => {
@@ -210,6 +215,8 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
* Dismissal handler - updates version tracking
*/
const handleBannerDismiss = useCallback((bannerId: string) => {
setDismissedLocalBanners((previous) => new Set(previous).add(bannerId))
// !! Do not continue use these version numbers or add new banners that don't have unique IDs. !!
// Banner versions are **deprecated**. Going forward, we are tracking which banners have
// been dismissed using the **banner ID**.
@@ -225,6 +232,65 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
}
}, [])
const clinePassPromoBanner = useMemo((): BannerData | undefined => {
if (
isBannerDismissed(CLINE_PASS_PROMO_BANNER_ID) ||
dismissedLocalBanners.has(CLINE_PASS_PROMO_BANNER_ID)
) {
return undefined
}
const dismissPromoBanner = () => handleBannerDismiss(CLINE_PASS_PROMO_BANNER_ID)
const subscriptionUrl = buildClinePassSubscriptionUrl(clineUser?.appBaseUrl)
return {
id: CLINE_PASS_PROMO_BANNER_ID,
icon: <Sparkles className="size-4 text-[var(--vscode-charts-yellow)]" />,
title: "Try ClinePass",
description: (
<div className="flex flex-col gap-2">
<p className="m-0">
A $9.99/month subscription for the latest open-weights models, at much lower cost than
paying for direct API access.
</p>
<div>
<Button
onClick={() => {
UiServiceClient.openUrl({ value: subscriptionUrl }).catch(console.error)
}}
size="sm">
Get ClinePass
</Button>
</div>
<button
className="w-fit cursor-pointer border-0 bg-transparent p-0 text-left text-xs text-[var(--vscode-textLink-foreground)] underline hover:cursor-pointer hover:text-[var(--vscode-textLink-activeForeground,var(--vscode-textLink-foreground))]"
onClick={async () => {
try {
await handleFieldsChange({
planModeApiProvider: "cline-pass",
actModeApiProvider: "cline-pass",
})
navigateToSettings("api-config")
} catch (error) {
console.error("Failed to switch to ClinePass:", error)
}
}}
type="button">
Switch to ClinePass provider to access subscription.
</button>
</div>
),
onDismiss: dismissPromoBanner,
}
}, [
clineUser?.appBaseUrl,
dismissedLocalBanners,
handleBannerDismiss,
handleFieldsChange,
isBannerDismissed,
navigateToSettings,
])
/**
* Build array of active banners for carousel
* Combines hardcoded banners (bannerConfig) with dynamic banners from extension state
@@ -247,8 +313,9 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
)
// Combine both sources: extension state banners first, then hardcoded banners
return [...extensionStateBanners, ...hardcodedBanners]
}, [bannerConfig, banners, clineUser, handleBannerAction, handleBannerDismiss])
const carouselBanners = [...extensionStateBanners, ...hardcodedBanners]
return clinePassPromoBanner ? [clinePassPromoBanner, ...carouselBanners] : carouselBanners
}, [bannerConfig, banners, clinePassPromoBanner, handleBannerAction, handleBannerDismiss])
return (
<div className="flex flex-col flex-1 w-full h-full p-0 m-0">
@@ -260,10 +327,10 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
welcomeBanners={welcomeBanners}
/>
<div className="overflow-y-auto flex flex-col pb-2.5">
<HomeHeader shouldShowQuickWins={shouldShowQuickWins} />
{!showWhatsNewModal && (
<>
<BannerCarousel banners={activeBanners} />
<HomeHeader shouldShowQuickWins={shouldShowQuickWins} />
{!shouldShowQuickWins && taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
{/* Quick launch worktree button */}
{isGitRepo && worktreesEnabled?.featureFlag && worktreesEnabled?.user && (
@@ -313,6 +380,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
)}
</>
)}
{showWhatsNewModal && <HomeHeader shouldShowQuickWins={shouldShowQuickWins} />}
</div>
<SuggestedTasks shouldShowQuickWins={shouldShowQuickWins} />
@@ -49,17 +49,22 @@ const BannerCardContent: React.FC<BannerCardContentProps> = ({ banner, isActive,
}}>
{/* Title with optional icon */}
<h3
className={cn("font-semibold mb-2 flex items-center gap-2 text-base pr-0", {
className={cn("font-semibold mb-2 flex items-center text-base pr-0", {
"gap-2": banner.icon,
"pr-6": showDismissButton,
})}>
<span className="shrink-0">{banner.icon}</span>
{banner.icon && <span className="shrink-0">{banner.icon}</span>}
{banner.title}
</h3>
{/* Description */}
<div className="text-sm text-description leading-relaxed [&>*:last-child]:mb-0 [&_a]:hover:underline">
{markdownContent}
</div>
{typeof banner.description === "string" ? (
<div className="text-sm text-description leading-relaxed [&>*:last-child]:mb-0 [&_a]:hover:underline">
{markdownContent}
</div>
) : (
<div className="text-sm text-description leading-relaxed">{banner.description}</div>
)}
{/* Action buttons */}
{banner.actions?.length ? (
@@ -7,15 +7,13 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Item, ItemContent, ItemDescription, ItemHeader, ItemMedia, ItemTitle } from "@/components/ui/item"
import { CLINE_PASS_FEATURE_FLAG } from "@/constants/featureFlags"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useHasFeatureFlag } from "@/hooks/useFeatureFlag"
import { cn } from "@/lib/utils"
import { AccountServiceClient, StateServiceClient } from "@/services/grpc-client"
import { setPendingClinePassSubscribe } from "./clinePassSubscribe"
import ApiConfigurationSection from "../settings/sections/ApiConfigurationSection"
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
import WelcomeView from "../welcome/WelcomeView"
import { setPendingClinePassSubscribe } from "./clinePassSubscribe"
import {
getCapabilities,
getClineUIOnboardingGroups,
@@ -301,8 +299,7 @@ const OnboardingStepContent = ({
const OnboardingViewContent = ({ onboardingModels }: { onboardingModels: OnboardingModelGroup }) => {
const { handleFieldsChange } = useApiConfigurationHandlers()
const { openRouterModels, hideSettings, hideAccount, setShowWelcome } = useExtensionState()
const isClinePassEnabled = useHasFeatureFlag(CLINE_PASS_FEATURE_FLAG)
const userTypeSelections = useMemo(() => getUserTypeSelections(isClinePassEnabled), [isClinePassEnabled])
const userTypeSelections = useMemo(() => getUserTypeSelections(true), [])
const [stepNumber, setStepNumber] = useState(0)
const [isActionLoading, setIsActionLoading] = useState(false)
@@ -4,9 +4,7 @@ import { EmptyRequest } from "@shared/proto/cline/common"
import type { ClineRecommendedModel } from "@shared/proto/cline/models"
import type { OnboardingModel, OnboardingModelGroup } from "@shared/proto/cline/state"
import { useEffect, useMemo, useState } from "react"
import { CLINE_PASS_FEATURE_FLAG } from "@/constants/featureFlags"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useHasFeatureFlag } from "@/hooks/useFeatureFlag"
import { ModelsServiceClient } from "@/services/grpc-client"
import { CLINEPASS_GROUP, getRecommendedModelsData, type RecommendedModelsData } from "./data-models"
@@ -51,7 +49,6 @@ type FetchState = { status: "loading" } | { status: "success"; data: Recommended
export function useOnboardingModels(): UseOnboardingModelsResult {
const { openRouterModels, clineModels, refreshClineModels } = useExtensionState()
const isClinePassEnabled = useHasFeatureFlag(CLINE_PASS_FEATURE_FLAG)
const [fetchState, setFetchState] = useState<FetchState>({ status: "loading" })
useEffect(() => {
@@ -61,7 +58,7 @@ export function useOnboardingModels(): UseOnboardingModelsResult {
try {
const response = await ModelsServiceClient.refreshClineRecommendedModelsRpc(EmptyRequest.create({}))
if (!cancelled) {
const data = getRecommendedModelsData(response, isClinePassEnabled)
const data = getRecommendedModelsData(response, true)
if (!data) {
setFetchState({ status: "empty" })
} else {
@@ -80,7 +77,7 @@ export function useOnboardingModels(): UseOnboardingModelsResult {
return () => {
cancelled = true
}
}, [isClinePassEnabled])
}, [])
useEffect(() => {
refreshClineModels()
@@ -1,75 +1,84 @@
import { StringRequest } from "@shared/proto/cline/common"
import PROVIDERS from "@shared/providers/providers.json"
import { Mode } from "@shared/storage/types"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useInterval } from "react-use"
import styled from "styled-components"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
import { CLINE_PASS_FEATURE_FLAG } from "@/constants/featureFlags"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useHasFeatureFlag } from "@/hooks/useFeatureFlag"
import { ModelsServiceClient } from "@/services/grpc-client"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
import { AIhubmixProvider } from "./providers/AihubmixProvider"
import { AnthropicProvider } from "./providers/AnthropicProvider"
import { AskSageProvider } from "./providers/AskSageProvider"
import { BasetenProvider } from "./providers/BasetenProvider"
import { BedrockProvider } from "./providers/BedrockProvider"
import { CerebrasProvider } from "./providers/CerebrasProvider"
import { ClaudeCodeProvider } from "./providers/ClaudeCodeProvider"
import { ClinePassProvider } from "./providers/ClinePassProvider"
import { ClineProvider } from "./providers/ClineProvider"
import { DeepSeekProvider } from "./providers/DeepSeekProvider"
import { DifyProvider } from "./providers/DifyProvider"
import { DoubaoProvider } from "./providers/DoubaoProvider"
import { FireworksProvider } from "./providers/FireworksProvider"
import { GeminiProvider } from "./providers/GeminiProvider"
import { GroqProvider } from "./providers/GroqProvider"
import { HicapProvider } from "./providers/HicapProvider"
import { HuaweiCloudMaasProvider } from "./providers/HuaweiCloudMaasProvider"
import { HuggingFaceProvider } from "./providers/HuggingFaceProvider"
import { LiteLlmProvider } from "./providers/LiteLlmProvider"
import { LMStudioProvider } from "./providers/LMStudioProvider"
import { MinimaxProvider } from "./providers/MiniMaxProvider"
import { MistralProvider } from "./providers/MistralProvider"
import { MoonshotProvider } from "./providers/MoonshotProvider"
import { NebiusProvider } from "./providers/NebiusProvider"
import { NousResearchProvider } from "./providers/NousresearchProvider"
import { OcaProvider } from "./providers/OcaProvider"
import { OllamaProvider } from "./providers/OllamaProvider"
import { OpenAICompatibleProvider } from "./providers/OpenAICompatible"
import { OpenAINativeProvider } from "./providers/OpenAINative"
import { OpenAiCodexProvider } from "./providers/OpenAiCodexProvider"
import { OpenRouterProvider } from "./providers/OpenRouterProvider"
import { QwenCodeProvider } from "./providers/QwenCodeProvider"
import { QwenProvider } from "./providers/QwenProvider"
import { RequestyProvider } from "./providers/RequestyProvider"
import { SambanovaProvider } from "./providers/SambanovaProvider"
import { SapAiCoreProvider } from "./providers/SapAiCoreProvider"
import { TogetherProvider } from "./providers/TogetherProvider"
import { VercelAIGatewayProvider } from "./providers/VercelAIGatewayProvider"
import { VertexProvider } from "./providers/VertexProvider"
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
import { WandbProvider } from "./providers/WandbProvider"
import { XaiProvider } from "./providers/XaiProvider"
import { ZAiProvider } from "./providers/ZAiProvider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
import type { ApiProvider } from "@shared/api";
import { modelsDevProviderOptions } from "@shared/models/models-dev-catalog";
import { StringRequest } from "@shared/proto/cline/common";
import type { Mode } from "@shared/storage/types";
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react";
import Fuse from "fuse.js";
import {
type KeyboardEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useInterval } from "react-use";
import styled from "styled-components";
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ModelsServiceClient } from "@/services/grpc-client";
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker";
import { AIhubmixProvider } from "./providers/AihubmixProvider";
import { AnthropicProvider } from "./providers/AnthropicProvider";
import { AskSageProvider } from "./providers/AskSageProvider";
import { BasetenProvider } from "./providers/BasetenProvider";
import { BedrockProvider } from "./providers/BedrockProvider";
import { CerebrasProvider } from "./providers/CerebrasProvider";
import { ClaudeCodeProvider } from "./providers/ClaudeCodeProvider";
import { ClineProvider } from "./providers/ClineProvider";
import { DeepSeekProvider } from "./providers/DeepSeekProvider";
import { DifyProvider } from "./providers/DifyProvider";
import { DoubaoProvider } from "./providers/DoubaoProvider";
import { FireworksProvider } from "./providers/FireworksProvider";
import { GeminiProvider } from "./providers/GeminiProvider";
import { GroqProvider } from "./providers/GroqProvider";
import { HicapProvider } from "./providers/HicapProvider";
import { HuaweiCloudMaasProvider } from "./providers/HuaweiCloudMaasProvider";
import { HuggingFaceProvider } from "./providers/HuggingFaceProvider";
import { LiteLlmProvider } from "./providers/LiteLlmProvider";
import { LMStudioProvider } from "./providers/LMStudioProvider";
import { MinimaxProvider } from "./providers/MiniMaxProvider";
import { MistralProvider } from "./providers/MistralProvider";
import { MoonshotProvider } from "./providers/MoonshotProvider";
import { NebiusProvider } from "./providers/NebiusProvider";
import { NousResearchProvider } from "./providers/NousresearchProvider";
import { OcaProvider } from "./providers/OcaProvider";
import { OllamaProvider } from "./providers/OllamaProvider";
import { OpenAICompatibleProvider } from "./providers/OpenAICompatible";
import { OpenAINativeProvider } from "./providers/OpenAINative";
import { OpenAiCodexProvider } from "./providers/OpenAiCodexProvider";
import { OpenRouterProvider } from "./providers/OpenRouterProvider";
import { QwenCodeProvider } from "./providers/QwenCodeProvider";
import { QwenProvider } from "./providers/QwenProvider";
import { RequestyProvider } from "./providers/RequestyProvider";
import { SambanovaProvider } from "./providers/SambanovaProvider";
import { SapAiCoreProvider } from "./providers/SapAiCoreProvider";
import { TogetherProvider } from "./providers/TogetherProvider";
import { VercelAIGatewayProvider } from "./providers/VercelAIGatewayProvider";
import { VertexProvider } from "./providers/VertexProvider";
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider";
import { WandbProvider } from "./providers/WandbProvider";
import { XaiProvider } from "./providers/XaiProvider";
import { ZAiProvider } from "./providers/ZAiProvider";
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers";
interface ApiOptionsProps {
showModelOptions: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
isPopup?: boolean
currentMode: Mode
initialModelTab?: "recommended" | "free"
showModelOptions: boolean;
apiErrorMessage?: string;
modelIdErrorMessage?: string;
isPopup?: boolean;
currentMode: Mode;
initialModelTab?: "recommended" | "free";
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
export const DROPDOWN_Z_INDEX = OPENROUTER_MODEL_PICKER_Z_INDEX + 2 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
export const DROPDOWN_Z_INDEX = OPENROUTER_MODEL_PICKER_Z_INDEX + 2; // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
export const DropdownContainer = styled.div<{ zIndex?: number }>`
position: relative;
@@ -81,14 +90,14 @@ export const DropdownContainer = styled.div<{ zIndex?: number }>`
top: 100% !important;
bottom: auto !important;
}
`
`;
declare module "vscode" {
interface LanguageModelChatSelector {
vendor?: string
family?: string
version?: string
id?: string
vendor?: string;
family?: string;
version?: string;
id?: string;
}
}
@@ -101,14 +110,17 @@ const ApiOptions = ({
initialModelTab,
}: ApiOptionsProps) => {
// Use full context state for immediate save payload
const { apiConfiguration, remoteConfigSettings } = useExtensionState()
const isClinePassEnabled = useHasFeatureFlag(CLINE_PASS_FEATURE_FLAG)
const { apiConfiguration, remoteConfigSettings } = useExtensionState();
const { selectedProvider } = normalizeApiConfiguration(apiConfiguration, currentMode, { isClinePassEnabled })
const { selectedProvider } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
{ isClinePassEnabled: true },
);
const { handleModeFieldChange } = useApiConfigurationHandlers()
const { handleModeFieldChange } = useApiConfigurationHandlers();
const [_ollamaModels, setOllamaModels] = useState<string[]>([])
const [_ollamaModels, setOllamaModels] = useState<string[]>([]);
// Poll ollama/vscode-lm models
const requestLocalModels = useCallback(async () => {
@@ -118,141 +130,179 @@ const ApiOptions = ({
StringRequest.create({
value: apiConfiguration?.ollamaBaseUrl || "",
}),
)
if (response && response.values) {
setOllamaModels(response.values)
);
if (response?.values) {
setOllamaModels(response.values);
}
} catch (error) {
console.error("Failed to fetch Ollama models:", error)
setOllamaModels([])
console.error("Failed to fetch Ollama models:", error);
setOllamaModels([]);
}
}
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl])
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl]);
useEffect(() => {
if (selectedProvider === "ollama") {
requestLocalModels()
requestLocalModels();
}
}, [selectedProvider, requestLocalModels])
useInterval(requestLocalModels, selectedProvider === "ollama" ? 2000 : null)
}, [selectedProvider, requestLocalModels]);
useInterval(requestLocalModels, selectedProvider === "ollama" ? 2000 : null);
// Provider search state
const [searchTerm, setSearchTerm] = useState("")
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 [searchTerm, setSearchTerm] = useState("");
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 providerOptions = useMemo(() => {
let providers = PROVIDERS.list
if (!isClinePassEnabled) {
providers = providers.filter((option) => option.value !== "cline-pass")
}
let providers = [...modelsDevProviderOptions];
// Filter by platform
if (PLATFORM_CONFIG.type !== PlatformType.VSCODE) {
// Don't include VS Code LM API for non-VSCode platforms
providers = providers.filter((option) => option.value !== "vscode-lm")
providers = providers.filter((option) => option.value !== "vscode-lm");
}
// Filter by remote config if remoteConfiguredProviders is set
const remoteProviders: string[] = remoteConfigSettings?.remoteConfiguredProviders || []
const remoteProviders: string[] =
remoteConfigSettings?.remoteConfiguredProviders || [];
if (remoteProviders.length > 0) {
providers = providers.filter((option) => remoteProviders.includes(option.value))
const effectiveRemoteProviders =
remoteProviders.includes("cline-pass") &&
!remoteProviders.includes("cline")
? [...remoteProviders, "cline"]
: remoteProviders;
providers = providers.filter((option) =>
effectiveRemoteProviders.includes(option.value),
);
}
return providers
}, [isClinePassEnabled, remoteConfigSettings])
return providers;
}, [remoteConfigSettings]);
const getProviderDisplayLabel = useCallback(
(option: (typeof modelsDevProviderOptions)[number]) => {
return option.value === "cline" ? "Cline Usage-Billing" : option.label;
},
[],
);
const currentProviderLabel = useMemo(() => {
return providerOptions.find((option) => option.value === selectedProvider)?.label || selectedProvider
}, [providerOptions, selectedProvider])
const selectedOption = providerOptions.find(
(option) => option.value === selectedProvider,
);
return selectedOption
? getProviderDisplayLabel(selectedOption)
: selectedProvider;
}, [getProviderDisplayLabel, providerOptions, selectedProvider]);
// Sync search term with current provider when not searching
useEffect(() => {
if (!isDropdownVisible) {
setSearchTerm(currentProviderLabel)
setSearchTerm(currentProviderLabel);
}
}, [currentProviderLabel, isDropdownVisible])
}, [currentProviderLabel, isDropdownVisible]);
const searchableItems = useMemo(() => {
return providerOptions.map((option) => ({
value: option.value,
html: option.label,
}))
}, [providerOptions])
html: getProviderDisplayLabel(option),
searchText:
option.value === "cline"
? "Cline Usage Billing usage based pay as you go"
: option.value === "cline-pass"
? "ClinePass subscription included models"
: option.label,
}));
}, [getProviderDisplayLabel, providerOptions]);
const fuse = useMemo(() => {
return new Fuse(searchableItems, {
keys: ["html"],
keys: ["html", "searchText"],
threshold: 0.3,
shouldSort: true,
isCaseSensitive: false,
ignoreLocation: false,
includeMatches: true,
minMatchCharLength: 1,
})
}, [searchableItems])
});
}, [searchableItems]);
const providerSearchResults = useMemo(() => {
return searchTerm && searchTerm !== currentProviderLabel ? fuse.search(searchTerm)?.map((r) => r.item) : searchableItems
}, [searchableItems, searchTerm, fuse, currentProviderLabel])
return searchTerm && searchTerm !== currentProviderLabel
? fuse.search(searchTerm)?.map((r) => r.item)
: searchableItems;
}, [searchableItems, searchTerm, fuse, currentProviderLabel]);
const handleProviderChange = (newProvider: string) => {
handleModeFieldChange({ plan: "planModeApiProvider", act: "actModeApiProvider" }, newProvider as any, currentMode)
setIsDropdownVisible(false)
setSelectedIndex(-1)
}
const handleProviderChange = (newProvider: ApiProvider) => {
handleModeFieldChange(
{ plan: "planModeApiProvider", act: "actModeApiProvider" },
newProvider,
currentMode,
);
setIsDropdownVisible(false);
setSelectedIndex(-1);
};
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (!isDropdownVisible) {
return
return;
}
switch (event.key) {
case "ArrowDown":
event.preventDefault()
setSelectedIndex((prev) => (prev < providerSearchResults.length - 1 ? prev + 1 : prev))
break
event.preventDefault();
setSelectedIndex((prev) =>
prev < providerSearchResults.length - 1 ? prev + 1 : prev,
);
break;
case "ArrowUp":
event.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
event.preventDefault();
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
break;
case "Enter":
event.preventDefault()
if (selectedIndex >= 0 && selectedIndex < providerSearchResults.length) {
handleProviderChange(providerSearchResults[selectedIndex].value)
event.preventDefault();
if (
selectedIndex >= 0 &&
selectedIndex < providerSearchResults.length
) {
handleProviderChange(providerSearchResults[selectedIndex].value);
}
break
break;
case "Escape":
setIsDropdownVisible(false)
setSelectedIndex(-1)
setSearchTerm(currentProviderLabel)
break
setIsDropdownVisible(false);
setSelectedIndex(-1);
setSearchTerm(currentProviderLabel);
break;
}
}
};
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownVisible(false)
setSearchTerm(currentProviderLabel)
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDropdownVisible(false);
setSearchTerm(currentProviderLabel);
}
}
};
document.addEventListener("mousedown", handleClickOutside)
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [currentProviderLabel])
document.removeEventListener("mousedown", handleClickOutside);
};
}, [currentProviderLabel]);
// Reset selection when search term changes
useEffect(() => {
setSelectedIndex(-1)
void searchTerm;
setSelectedIndex(-1);
if (dropdownListRef.current) {
dropdownListRef.current.scrollTop = 0
dropdownListRef.current.scrollTop = 0;
}
}, [searchTerm])
}, [searchTerm]);
// Scroll selected item into view
useEffect(() => {
@@ -260,9 +310,9 @@ const ApiOptions = ({
itemRefs.current[selectedIndex]?.scrollIntoView({
block: "nearest",
behavior: "smooth",
})
});
}
}, [selectedIndex])
}, [selectedIndex]);
/*
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
@@ -274,7 +324,14 @@ const ApiOptions = ({
*/
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5, marginBottom: isPopup ? -10 : 0 }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 5,
marginBottom: isPopup ? -10 : 0,
}}
>
<style>
{`
.provider-item-highlight {
@@ -284,7 +341,8 @@ const ApiOptions = ({
`}
</style>
<DropdownContainer className="dropdown-container">
{remoteConfigSettings?.remoteConfiguredProviders && remoteConfigSettings.remoteConfiguredProviders.length > 0 ? (
{remoteConfigSettings?.remoteConfiguredProviders &&
remoteConfigSettings.remoteConfiguredProviders.length > 0 ? (
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2 mb-1">
@@ -294,7 +352,10 @@ const ApiOptions = ({
<i className="codicon codicon-lock text-description text-sm" />
</div>
</TooltipTrigger>
<TooltipContent>Provider options are managed by your organization's remote configuration</TooltipContent>
<TooltipContent>
Provider options are managed by your organization's remote
configuration
</TooltipContent>
</Tooltip>
) : (
<label htmlFor="api-provider">
@@ -306,12 +367,12 @@ const ApiOptions = ({
data-testid="provider-selector-input"
id="api-provider"
onFocus={() => {
setIsDropdownVisible(true)
setSearchTerm("")
setIsDropdownVisible(true);
setSearchTerm("");
}}
onInput={(e) => {
setSearchTerm((e.target as HTMLInputElement)?.value || "")
setIsDropdownVisible(true)
setSearchTerm((e.target as HTMLInputElement)?.value || "");
setIsDropdownVisible(true);
}}
onKeyDown={handleKeyDown}
placeholder="Search and select provider..."
@@ -322,22 +383,28 @@ const ApiOptions = ({
position: "relative",
minWidth: 130,
}}
value={searchTerm}>
value={searchTerm}
>
{searchTerm && searchTerm !== currentProviderLabel && (
<div
<button
aria-label="Clear search"
className="input-icon-button codicon codicon-close"
onClick={() => {
setSearchTerm("")
setIsDropdownVisible(true)
setSearchTerm("");
setIsDropdownVisible(true);
}}
slot="end"
style={{
background: "none",
border: 0,
color: "inherit",
cursor: "pointer",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
type="button"
/>
)}
</VSCodeTextField>
@@ -351,9 +418,10 @@ const ApiOptions = ({
onClick={() => handleProviderChange(item.value)}
onMouseEnter={() => setSelectedIndex(index)}
ref={(el) => {
itemRefs.current[index] = el
itemRefs.current[index] = el;
}}
role="option">
role="option"
>
<span>{item.html}</span>
</ProviderDropdownItem>
))}
@@ -363,175 +431,333 @@ const ApiOptions = ({
</DropdownContainer>
{apiConfiguration && selectedProvider === "hicap" && (
<HicapProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
{apiConfiguration && selectedProvider === "cline" && (
<ClineProvider
<HicapProvider
currentMode={currentMode}
initialModelTab={initialModelTab}
isClinePassEnabled={isClinePassEnabled}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && isClinePassEnabled && selectedProvider === "cline-pass" && (
<ClinePassProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
{apiConfiguration &&
(selectedProvider === "cline" || selectedProvider === "cline-pass") && (
<ClineProvider
currentMode={currentMode}
initialModelTab={initialModelTab}
isClinePassEnabled={true}
isPopup={isPopup}
selectedProvider={selectedProvider}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "asksage" && (
<AskSageProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<AskSageProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "anthropic" && (
<AnthropicProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<AnthropicProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "claude-code" && (
<ClaudeCodeProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<ClaudeCodeProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "openai-native" && (
<OpenAINativeProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<OpenAINativeProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "openai-codex" && (
<OpenAiCodexProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<OpenAiCodexProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "qwen" && (
<QwenProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<QwenProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "qwen-code" && (
<QwenCodeProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<QwenCodeProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "doubao" && (
<DoubaoProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<DoubaoProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "mistral" && (
<MistralProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<MistralProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "openrouter" && (
<OpenRouterProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<OpenRouterProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "deepseek" && (
<DeepSeekProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<DeepSeekProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "together" && (
<TogetherProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<TogetherProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "openai" && (
<OpenAICompatibleProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<OpenAICompatibleProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "vercel-ai-gateway" && (
<VercelAIGatewayProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<VercelAIGatewayProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "sambanova" && (
<SambanovaProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<SambanovaProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "bedrock" && (
<BedrockProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<BedrockProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "vertex" && (
<VertexProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<VertexProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "gemini" && (
<GeminiProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<GeminiProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "requesty" && (
<RequestyProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<RequestyProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "fireworks" && (
<FireworksProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<FireworksProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider currentMode={currentMode} />}
{apiConfiguration && selectedProvider === "vscode-lm" && (
<VSCodeLmProvider currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "groq" && (
<GroqProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<GroqProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "baseten" && (
<BasetenProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<BasetenProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "litellm" && (
<LiteLlmProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<LiteLlmProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "lmstudio" && (
<LMStudioProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<LMStudioProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "ollama" && (
<OllamaProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<OllamaProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "moonshot" && (
<MoonshotProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<MoonshotProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "huggingface" && (
<HuggingFaceProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<HuggingFaceProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "nebius" && (
<NebiusProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<NebiusProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "wandb" && (
<WandbProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<WandbProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "xai" && (
<XaiProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<XaiProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "cerebras" && (
<CerebrasProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<CerebrasProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "sapaicore" && (
<SapAiCoreProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<SapAiCoreProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "huawei-cloud-maas" && (
<HuaweiCloudMaasProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<HuaweiCloudMaasProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "dify" && (
<DifyProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<DifyProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "zai" && (
<ZAiProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<ZAiProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "minimax" && (
<MinimaxProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<MinimaxProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "nousResearch" && (
<NousResearchProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<NousResearchProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "oca" && <OcaProvider currentMode={currentMode} isPopup={isPopup} />}
{apiConfiguration && selectedProvider === "oca" && (
<OcaProvider currentMode={currentMode} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "aihubmix" && (
<AIhubmixProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<AIhubmixProvider
currentMode={currentMode}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiErrorMessage && (
@@ -540,7 +766,8 @@ const ApiOptions = ({
margin: "-10px 0 4px 0",
fontSize: 12,
color: "var(--vscode-errorForeground)",
}}>
}}
>
{apiErrorMessage}
</p>
)}
@@ -550,20 +777,21 @@ const ApiOptions = ({
margin: "-10px 0 4px 0",
fontSize: 12,
color: "var(--vscode-errorForeground)",
}}>
}}
>
{modelIdErrorMessage}
</p>
)}
</div>
)
}
);
};
export default ApiOptions
export default ApiOptions;
const ProviderDropdownWrapper = styled.div`
position: relative;
width: 100%;
`
`;
const ProviderDropdownList = styled.div`
position: absolute;
@@ -577,7 +805,7 @@ const ProviderDropdownList = styled.div`
z-index: ${DROPDOWN_Z_INDEX - 1};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
`
`;
const ProviderDropdownItem = styled.div<{ isSelected: boolean }>`
padding: 5px 10px;
@@ -590,4 +818,4 @@ const ProviderDropdownItem = styled.div<{ isSelected: boolean }>`
&:hover {
background-color: var(--vscode-list-activeSelectionBackground);
}
`
`;
@@ -29,7 +29,7 @@ export const ClineAccountInfoCard = () => {
<div className="max-w-[600px]">
{user ? (
<VSCodeButton appearance="secondary" onClick={handleShowAccount}>
View Billing & Usage
View Billing History
</VSCodeButton>
) : (
<div>
@@ -37,7 +37,7 @@ export const ClineAccountInfoCard = () => {
Sign Up with Cline
{isLoading && (
<span className="ml-1 animate-spin">
<span className="codicon codicon-refresh"></span>
<span className="codicon codicon-refresh" />
</span>
)}
</VSCodeButton>
@@ -189,8 +189,9 @@ export const ModelInfoView = ({
const [advancedExpanded, setAdvancedExpanded] = useState(false)
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
const hidePricing = selectedModelId.trim().toLowerCase().startsWith("cline-pass/")
const hasThinkingConfig = hasThinkingBudget(modelInfo)
const hasTiers = !!modelInfo.tiers && modelInfo.tiers.length > 0
const hasTiers = !hidePricing && !!modelInfo.tiers && modelInfo.tiers.length > 0
// Capability checks
const hasImages = supportsImages(modelInfo)
@@ -198,7 +199,8 @@ export const ModelInfoView = ({
const hasCaching = !isGemini && supportsPromptCache(modelInfo)
// Check if we have cache pricing to show in Advanced section
const hasCachePricing = modelInfo.supportsPromptCache && (modelInfo.cacheWritesPrice || modelInfo.cacheReadsPrice)
const hasCachePricing =
!hidePricing && modelInfo.supportsPromptCache && (modelInfo.cacheWritesPrice || modelInfo.cacheReadsPrice)
return (
<div style={{ marginTop: 4 }}>
@@ -215,13 +217,13 @@ export const ModelInfoView = ({
<InfoValue>{formatCompactContext(modelInfo.contextWindow)}</InfoValue>
</InfoItem>
)}
{modelInfo.inputPrice !== undefined && (
{!hidePricing && modelInfo.inputPrice !== undefined && (
<InfoItem>
<InfoLabel>Input: </InfoLabel>
<InfoValue>{formatCompactPrice(modelInfo.inputPrice)}</InfoValue>
</InfoItem>
)}
{modelInfo.outputPrice !== undefined && (
{!hidePricing && modelInfo.outputPrice !== undefined && (
<InfoItem>
<InfoLabel>Output: </InfoLabel>
<InfoValue>
@@ -1,16 +1,22 @@
import { ModelInfo } from "@shared/api"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import type { ModelInfo } from "@shared/api";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import styled from "styled-components";
export type ModelSelectorChangeEvent = Event & {
target: EventTarget & { value: string };
};
/**
* Container for dropdowns that ensures proper z-index handling
* This is necessary to ensure dropdown opens downward
*/
export const DropdownContainer = styled.div.attrs<{ zIndex?: number }>(({ zIndex }) => ({
style: {
zIndex: zIndex || 1000,
},
}))`
export const DropdownContainer = styled.div.attrs<{ zIndex?: number }>(
({ zIndex }) => ({
style: {
zIndex: zIndex || 1000,
},
}),
)`
position: relative;
// Force dropdowns to open downward
@@ -19,17 +25,17 @@ export const DropdownContainer = styled.div.attrs<{ zIndex?: number }>(({ zIndex
top: 100% !important;
bottom: auto !important;
}
`
`;
/**
* Props for the ModelSelector component
*/
interface ModelSelectorProps {
models: Record<string, ModelInfo>
selectedModelId: string | undefined
onChange: (e: any) => void
zIndex?: number
label?: string
models: Record<string, ModelInfo>;
selectedModelId: string | undefined;
onChange: (e: ModelSelectorChangeEvent) => void;
zIndex?: number;
label?: string;
}
/*
@@ -46,20 +52,35 @@ OG Saoud Note:
/**
* A reusable component for selecting models from a dropdown
*/
export const ModelSelector = ({ models, selectedModelId, onChange, zIndex, label = "Model" }: ModelSelectorProps) => {
export const ModelSelector = ({
models,
selectedModelId,
onChange,
zIndex,
label = "Model",
}: ModelSelectorProps) => {
return (
<DropdownContainer className="dropdown-container" zIndex={zIndex}>
<label htmlFor="model-id">
<span className="font-medium">{label}</span>
</label>
<VSCodeDropdown className="w-full" id="model-id" onChange={onChange} value={selectedModelId}>
<VSCodeDropdown
className="w-full"
id="model-id"
onChange={onChange}
value={selectedModelId}
>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(models).map((modelId) => (
<VSCodeOption className="break-words whitespace-normal max-w-full" key={modelId} value={modelId}>
<VSCodeOption
className="break-words whitespace-normal max-w-full"
key={modelId}
value={modelId}
>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
</DropdownContainer>
)
}
);
};
@@ -1,17 +1,27 @@
import { ANTHROPIC_FAST_MODE_SUFFIX, anthropicModels, CLAUDE_SONNET_1M_SUFFIX } from "@shared/api"
import type { Mode } from "@shared/storage/types"
import { isClaudeOpusAdaptiveThinkingModel, resolveClaudeOpusAdaptiveThinking } from "@shared/utils/reasoning-support"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ContextWindowSwitcher } from "../common/ContextWindowSwitcher"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { RemotelyConfiguredInputWrapper } from "../common/RemotelyConfiguredInputWrapper"
import ReasoningEffortSelector from "../ReasoningEffortSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { getModeSpecificFields, normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import {
ANTHROPIC_FAST_MODE_SUFFIX,
CLAUDE_SONNET_1M_SUFFIX,
} from "@shared/api";
import { modelsDevAnthropicModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import {
isClaudeOpusAdaptiveThinkingModel,
resolveClaudeOpusAdaptiveThinking,
} from "@shared/utils/reasoning-support";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { BaseUrlField } from "../common/BaseUrlField";
import { ContextWindowSwitcher } from "../common/ContextWindowSwitcher";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { RemotelyConfiguredInputWrapper } from "../common/RemotelyConfiguredInputWrapper";
import ReasoningEffortSelector from "../ReasoningEffortSelector";
import ThinkingBudgetSlider from "../ThinkingBudgetSlider";
import {
getModeSpecificFields,
normalizeApiConfiguration,
} from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
// Anthropic models that support thinking/reasoning mode
export const SUPPORTED_ANTHROPIC_THINKING_MODELS = [
@@ -25,35 +35,51 @@ export const SUPPORTED_ANTHROPIC_THINKING_MODELS = [
"claude-sonnet-4-5-20250929",
`claude-sonnet-4-5-20250929${CLAUDE_SONNET_1M_SUFFIX}`,
"claude-haiku-4-5-20251001",
]
];
/**
* Props for the AnthropicProvider component
*/
interface AnthropicProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The Anthropic provider configuration component
*/
export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: AnthropicProviderProps) => {
const { apiConfiguration, remoteConfigSettings } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
export const AnthropicProvider = ({
showModelOptions,
isPopup,
currentMode,
}: AnthropicProviderProps) => {
const { apiConfiguration, remoteConfigSettings } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
const modeFields = getModeSpecificFields(apiConfiguration, currentMode);
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const isAdaptiveThinkingModel = isClaudeOpusAdaptiveThinkingModel(selectedModelId)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
const isAdaptiveThinkingModel =
isClaudeOpusAdaptiveThinkingModel(selectedModelId);
const adaptiveThinkingDefaultEffort =
resolveClaudeOpusAdaptiveThinking(modeFields.reasoningEffort, modeFields.thinkingBudgetTokens).effort ?? "none"
resolveClaudeOpusAdaptiveThinking(
modeFields.reasoningEffort,
modeFields.thinkingBudgetTokens,
).effort ?? "none";
// Helper function for model switching
const handleModelChange = (modelId: string) => {
handleModeFieldChange({ plan: "planModeApiModelId", act: "actModeApiModelId" }, modelId, currentMode)
}
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
modelId,
currentMode,
);
};
return (
<div>
@@ -64,7 +90,9 @@ export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: An
signupUrl="https://console.anthropic.com/settings/keys"
/>
<RemotelyConfiguredInputWrapper hidden={remoteConfigSettings?.anthropicBaseUrl === undefined}>
<RemotelyConfiguredInputWrapper
hidden={remoteConfigSettings?.anthropicBaseUrl === undefined}
>
<BaseUrlField
disabled={!!remoteConfigSettings?.anthropicBaseUrl}
initialValue={apiConfiguration?.anthropicBaseUrl}
@@ -79,7 +107,7 @@ export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: An
<>
<ModelSelector
label="Model"
models={anthropicModels}
models={modelsDevAnthropicModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
@@ -131,19 +159,28 @@ export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: An
{isAdaptiveThinkingModel ? (
<ReasoningEffortSelector
allowedEfforts={["none", "low", "medium", "high", "xhigh"] as const}
allowedEfforts={
["none", "low", "medium", "high", "xhigh"] as const
}
currentMode={currentMode}
defaultEffort={adaptiveThinkingDefaultEffort}
description="Use None to disable adaptive thinking. Higher effort increases response detail and token usage."
label="Adaptive Thinking"
/>
) : SUPPORTED_ANTHROPIC_THINKING_MODELS.includes(selectedModelId) ? (
<ThinkingBudgetSlider currentMode={currentMode} maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
<ThinkingBudgetSlider
currentMode={currentMode}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
) : null}
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,30 +1,38 @@
import { cerebrasModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevCerebrasModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the CerebrasProvider component
*/
interface CerebrasProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The Cerebras provider configuration component
*/
export const CerebrasProvider = ({ showModelOptions, isPopup, currentMode }: CerebrasProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const CerebrasProvider = ({
showModelOptions,
isPopup,
currentMode,
}: CerebrasProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -39,8 +47,8 @@ export const CerebrasProvider = ({ showModelOptions, isPopup, currentMode }: Cer
<>
<ModelSelector
label="Model"
models={cerebrasModels}
onChange={(e: any) =>
models={modelsDevCerebrasModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -50,9 +58,13 @@ export const CerebrasProvider = ({ showModelOptions, isPopup, currentMode }: Cer
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -6,14 +6,26 @@ import {
resolveClinePassModelInfo,
} from "@shared/api"
import { EmptyRequest } from "@shared/proto/cline/common"
import type { Mode } from "@shared/storage/types"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { ClineAccountInfoCard } from "../ClineAccountInfoCard"
import ClineModelPicker from "../ClineModelPicker"
import { ClineProvider } from "./ClineProvider"
export const ClinePassProvider: typeof ClineProvider = (props) => {
interface ClinePassProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showAccountCard?: boolean
}
export const ClinePassProvider = ({
showModelOptions,
isPopup,
currentMode,
showAccountCard = true,
}: ClinePassProviderProps) => {
const { openRouterModels } = useExtensionState()
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
const [clinePassRecommendedModels, setClinePassRecommendedModels] = useState<Record<string, ModelInfo> | undefined>(undefined)
@@ -63,18 +75,23 @@ export const ClinePassProvider: typeof ClineProvider = (props) => {
return (
<div>
<div style={{ marginBottom: 14, marginTop: 4 }}>
<ClineAccountInfoCard />
</div>
{showAccountCard && (
<div style={{ marginBottom: 14, marginTop: 4 }}>
<ClineAccountInfoCard />
</div>
)}
<ClineModelPicker
{...props}
defaultModelId={clinePassDefaultModel}
modelIdFieldPair={{ plan: "planModeClinePassModelId", act: "actModeClinePassModelId" }}
modelInfoFieldPair={{ plan: "planModeClinePassModelInfo", act: "actModeClinePassModelInfo" }}
models={clinePassModelOptions}
showFeaturedModels={false}
/>
{showModelOptions && (
<ClineModelPicker
currentMode={currentMode}
defaultModelId={clinePassDefaultModel}
isPopup={isPopup}
modelIdFieldPair={{ plan: "planModeClinePassModelId", act: "actModeClinePassModelId" }}
modelInfoFieldPair={{ plan: "planModeClinePassModelInfo", act: "actModeClinePassModelInfo" }}
models={clinePassModelOptions}
showFeaturedModels={false}
/>
)}
</div>
)
}
@@ -1,18 +1,28 @@
import { Mode } from "@shared/storage/types"
import type { ApiProvider } from "@shared/api"
import type { Mode } from "@shared/storage/types"
import styled from "styled-components"
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
import { useClineAuth } from "@/context/ClineAuthContext"
import { buildClinePassSubscriptionUrl } from "@/utils/clinePassSubscription"
import { ClineAccountInfoCard } from "../ClineAccountInfoCard"
import ClineModelPicker from "../ClineModelPicker"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { ClinePassProvider } from "./ClinePassProvider"
/**
* Props for the ClineProvider component
*/
interface ClineProviderProps {
export interface ClineProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
initialModelTab?: "recommended" | "free"
isClinePassEnabled?: boolean
selectedProvider?: ApiProvider
}
type ClineBillingRoute = "cline" | "cline-pass"
/**
* The Cline provider configuration component
*/
@@ -22,23 +32,128 @@ export const ClineProvider = ({
currentMode,
initialModelTab,
isClinePassEnabled,
selectedProvider,
}: ClineProviderProps) => {
const { handleModeFieldChange } = useApiConfigurationHandlers()
const { clineUser } = useClineAuth()
const activeRoute: ClineBillingRoute = selectedProvider === "cline-pass" && isClinePassEnabled ? "cline-pass" : "cline"
const clinePassSubscribeUrl = clineUser ? buildClinePassSubscriptionUrl(clineUser.appBaseUrl) : undefined
const handleRouteChange = async (route: ClineBillingRoute) => {
if (route === activeRoute) {
return
}
await handleModeFieldChange({ plan: "planModeApiProvider", act: "actModeApiProvider" }, route, currentMode)
}
return (
<div>
{/* Cline Account Info Card */}
<div style={{ marginBottom: 14, marginTop: 4 }}>
<ClineAccountInfoCard />
</div>
{isClinePassEnabled && (
<RouteContainer>
<RouteStatus>
{activeRoute === "cline-pass" ? (
<>
ClinePass lets you use the best open weights models.{" "}
<RouteLink
href="#"
onClick={(event) => {
event.preventDefault()
handleRouteChange("cline").catch((error) => console.error("Failed to switch to Cline:", error))
}}>
Switch to Cline Usage-Billing for other models.
</RouteLink>
</>
) : (
<>
Usage-Billing models bill to your Cline account balance.{" "}
<RouteLink
href="#"
onClick={(event) => {
event.preventDefault()
handleRouteChange("cline-pass").catch((error) =>
console.error("Failed to switch to ClinePass:", error),
)
}}>
Switch to ClinePass provider to access subscription.
</RouteLink>
</>
)}
</RouteStatus>
<RouteActions>
{activeRoute === "cline" && <ClineAccountInfoCard />}
{activeRoute === "cline-pass" && (
clinePassSubscribeUrl ? (
<VSCodeButtonLink appearance="secondary" href={clinePassSubscribeUrl}>
Manage ClinePass or See Usage
</VSCodeButtonLink>
) : (
<ClineAccountInfoCard />
)
)}
</RouteActions>
</RouteContainer>
)}
{!isClinePassEnabled && (
<StandaloneRouteActions>
<ClineAccountInfoCard />
</StandaloneRouteActions>
)}
{showModelOptions && (
<ClineModelPicker
currentMode={currentMode}
initialTab={initialModelTab}
isClinePassEnabled={isClinePassEnabled}
isPopup={isPopup}
showProviderRouting={true}
/>
activeRoute === "cline-pass" ? (
<ClinePassProvider
currentMode={currentMode}
isPopup={isPopup}
showAccountCard={false}
showModelOptions={showModelOptions}
/>
) : (
<ClineModelPicker
currentMode={currentMode}
initialTab={initialModelTab}
isClinePassEnabled={isClinePassEnabled}
isPopup={isPopup}
showProviderRouting={true}
/>
)
)}
</div>
)
}
const RouteContainer = styled.div`
margin-bottom: 10px;
`
const RouteStatus = styled.div`
font-size: 11px;
line-height: 1.35;
color: var(--vscode-descriptionForeground);
`
const RouteLink = styled.a`
color: var(--vscode-textLink-foreground);
font: inherit;
line-height: inherit;
cursor: pointer !important;
text-decoration: none;
&:hover {
color: var(--vscode-textLink-activeForeground, var(--vscode-textLink-foreground));
cursor: pointer !important;
text-decoration: underline;
}
`
const RouteActions = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
margin-top: 8px;
`
const StandaloneRouteActions = styled(RouteActions)`
margin: 4px 0 14px;
`
@@ -1,30 +1,47 @@
import { deepSeekModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevDeepSeekModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import ReasoningEffortSelector from "../ReasoningEffortSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
const DEEPSEEK_REASONING_EFFORT_MODELS = new Set([
"deepseek-v4-flash",
"deepseek-v4-pro",
"deepseek-reasoner",
]);
/**
* Props for the DeepSeekProvider component
*/
interface DeepSeekProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The DeepSeek provider configuration component
*/
export const DeepSeekProvider = ({ showModelOptions, isPopup, currentMode }: DeepSeekProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const DeepSeekProvider = ({
showModelOptions,
isPopup,
currentMode,
}: DeepSeekProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
const showReasoningEffort =
DEEPSEEK_REASONING_EFFORT_MODELS.has(selectedModelId);
return (
<div>
@@ -39,8 +56,8 @@ export const DeepSeekProvider = ({ showModelOptions, isPopup, currentMode }: Dee
<>
<ModelSelector
label="Model"
models={deepSeekModels}
onChange={(e: any) =>
models={modelsDevDeepSeekModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -50,9 +67,17 @@ export const DeepSeekProvider = ({ showModelOptions, isPopup, currentMode }: Dee
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
{showReasoningEffort && (
<ReasoningEffortSelector currentMode={currentMode} />
)}
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,30 +1,38 @@
import { doubaoModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevDoubaoModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the DoubaoProvider component
*/
interface DoubaoProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The ByteDance Doubao provider configuration component
*/
export const DoubaoProvider = ({ showModelOptions, isPopup, currentMode }: DoubaoProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const DoubaoProvider = ({
showModelOptions,
isPopup,
currentMode,
}: DoubaoProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -39,8 +47,8 @@ export const DoubaoProvider = ({ showModelOptions, isPopup, currentMode }: Douba
<>
<ModelSelector
label="Model"
models={doubaoModels}
onChange={(e: any) =>
models={modelsDevDoubaoModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -50,9 +58,13 @@ export const DoubaoProvider = ({ showModelOptions, isPopup, currentMode }: Douba
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,29 +1,37 @@
import { fireworksModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevFireworksModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the FireworksProvider component
*/
interface FireworksProviderProps {
currentMode: Mode
isPopup?: boolean
showModelOptions: boolean
currentMode: Mode;
isPopup?: boolean;
showModelOptions: boolean;
}
/**
* The Fireworks provider configuration component
*/
export const FireworksProvider = ({ currentMode, isPopup, showModelOptions }: FireworksProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleModeFieldChange, handleFieldChange } = useApiConfigurationHandlers()
export const FireworksProvider = ({
currentMode,
isPopup,
showModelOptions,
}: FireworksProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleModeFieldChange, handleFieldChange } =
useApiConfigurationHandlers();
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -33,23 +41,31 @@ export const FireworksProvider = ({ currentMode, isPopup, showModelOptions }: Fi
providerName="Fireworks"
signupUrl="https://fireworks.ai/"
/>
<ModelSelector
label="Model"
models={fireworksModels}
onChange={(e: any) => {
handleModeFieldChange(
{
plan: "planModeFireworksModelId",
act: "actModeFireworksModelId",
},
e.target.value,
currentMode,
)
}}
selectedModelId={selectedModelId}
/>
{showModelOptions && (
<>
<ModelSelector
label="Model"
models={modelsDevFireworksModels}
onChange={(e) => {
handleModeFieldChange(
{
plan: "planModeFireworksModelId",
act: "actModeFireworksModelId",
},
e.target.value,
currentMode,
);
}}
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,33 +1,45 @@
import { geminiModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import ReasoningEffortSelector from "../ReasoningEffortSelector"
import { normalizeApiConfiguration, supportsReasoningEffortForModelId } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevGeminiModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { BaseUrlField } from "../common/BaseUrlField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import ReasoningEffortSelector from "../ReasoningEffortSelector";
import {
normalizeApiConfiguration,
supportsReasoningEffortForModelId,
} from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the GeminiProvider component
*/
interface GeminiProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The Gemini provider configuration component
*/
export const GeminiProvider = ({ showModelOptions, isPopup, currentMode }: GeminiProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const GeminiProvider = ({
showModelOptions,
isPopup,
currentMode,
}: GeminiProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const showReasoningEffort = supportsReasoningEffortForModelId(selectedModelId)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
const showReasoningEffort =
supportsReasoningEffortForModelId(selectedModelId);
return (
<div>
@@ -49,8 +61,8 @@ export const GeminiProvider = ({ showModelOptions, isPopup, currentMode }: Gemin
<>
<ModelSelector
label="Model"
models={geminiModels}
onChange={(e: any) =>
models={modelsDevGeminiModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -60,11 +72,17 @@ export const GeminiProvider = ({ showModelOptions, isPopup, currentMode }: Gemin
selectedModelId={selectedModelId}
/>
{showReasoningEffort && <ReasoningEffortSelector currentMode={currentMode} />}
{showReasoningEffort && (
<ReasoningEffortSelector currentMode={currentMode} />
)}
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,23 +1,32 @@
import { huaweiCloudMaasModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { getModelsDevProviderModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
interface HuaweiCloudMaasProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
export const HuaweiCloudMaasProvider = ({ showModelOptions, isPopup, currentMode }: HuaweiCloudMaasProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldsChange } = useApiConfigurationHandlers()
export const HuaweiCloudMaasProvider = ({
showModelOptions,
isPopup,
currentMode,
}: HuaweiCloudMaasProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldsChange } =
useApiConfigurationHandlers();
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
const huaweiCloudMaasModels = getModelsDevProviderModels("huawei-cloud-maas");
return (
<div>
@@ -32,12 +41,18 @@ export const HuaweiCloudMaasProvider = ({ showModelOptions, isPopup, currentMode
<ModelSelector
label="Model"
models={huaweiCloudMaasModels}
onChange={(e: any) => {
const modelId = e.target.value
const modelInfo = huaweiCloudMaasModels[modelId as keyof typeof huaweiCloudMaasModels]
onChange={(e) => {
const modelId = e.target.value;
const modelInfo =
huaweiCloudMaasModels[
modelId as keyof typeof huaweiCloudMaasModels
];
handleModeFieldsChange(
{
apiModelId: { plan: "planModeApiModelId", act: "actModeApiModelId" },
apiModelId: {
plan: "planModeApiModelId",
act: "actModeApiModelId",
},
huaweiCloudMaaSModelId: {
plan: "planModeHuaweiCloudMaasModelId",
act: "actModeHuaweiCloudMaasModelId",
@@ -53,13 +68,17 @@ export const HuaweiCloudMaasProvider = ({ showModelOptions, isPopup, currentMode
huaweiCloudMaaSModelInfo: modelInfo,
},
currentMode,
)
);
}}
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,47 +1,66 @@
import { minimaxModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer, ModelSelector } from "../common/ModelSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevMinimaxModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { DropdownContainer, ModelSelector } from "../common/ModelSelector";
import ThinkingBudgetSlider from "../ThinkingBudgetSlider";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the MinimaxProvider component
*/
interface MinimaxProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The Minimax AI Studio provider configuration component
*/
export const MinimaxProvider = ({ showModelOptions, isPopup, currentMode }: MinimaxProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const MinimaxProvider = ({
showModelOptions,
isPopup,
currentMode,
}: MinimaxProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<DropdownContainer
className="dropdown-container"
style={{ position: "inherit" }}
>
<label htmlFor="minimax-entrypoint">
<span style={{ fontWeight: 500, marginTop: 5 }}>MiniMax Entrypoint</span>
<span style={{ fontWeight: 500, marginTop: 5 }}>
MiniMax Entrypoint
</span>
</label>
<VSCodeDropdown
id="minimax-entrypoint"
onChange={(e) => handleFieldChange("minimaxApiLine", (e.target as any).value)}
onChange={(e) =>
handleFieldChange(
"minimaxApiLine",
(e.target as EventTarget & { value: string }).value,
)
}
style={{
minWidth: 130,
position: "relative",
}}
value={apiConfiguration?.minimaxApiLine || "international"}>
value={apiConfiguration?.minimaxApiLine || "international"}
>
<VSCodeOption value="international">api.minimax.io</VSCodeOption>
<VSCodeOption value="china">api.minimaxi.com</VSCodeOption>
</VSCodeDropdown>
@@ -51,9 +70,11 @@ export const MinimaxProvider = ({ showModelOptions, isPopup, currentMode }: Mini
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Select the API endpoint according to your region: <code>api.minimaxi.com</code> for China, or{" "}
<code>api.minimax.io</code> for all other locations.
}}
>
Select the API endpoint according to your region:{" "}
<code>api.minimaxi.com</code> for China, or <code>api.minimax.io</code>{" "}
for all other locations.
</p>
<ApiKeyField
initialValue={apiConfiguration?.minimaxApiKey || ""}
@@ -70,8 +91,8 @@ export const MinimaxProvider = ({ showModelOptions, isPopup, currentMode }: Mini
<>
<ModelSelector
label="Model"
models={minimaxModels}
onChange={(e: any) =>
models={modelsDevMinimaxModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -82,12 +103,19 @@ export const MinimaxProvider = ({ showModelOptions, isPopup, currentMode }: Mini
/>
{selectedModelInfo?.supportsReasoning && (
<ThinkingBudgetSlider currentMode={currentMode} showEnableToggle={false} />
<ThinkingBudgetSlider
currentMode={currentMode}
showEnableToggle={false}
/>
)}
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,30 +1,38 @@
import { mistralModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevMistralModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the MistralProvider component
*/
interface MistralProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The Mistral provider configuration component
*/
export const MistralProvider = ({ showModelOptions, isPopup, currentMode }: MistralProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const MistralProvider = ({
showModelOptions,
isPopup,
currentMode,
}: MistralProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -39,8 +47,8 @@ export const MistralProvider = ({ showModelOptions, isPopup, currentMode }: Mist
<>
<ModelSelector
label="Model"
models={mistralModels}
onChange={(e: any) =>
models={modelsDevMistralModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -50,9 +58,13 @@ export const MistralProvider = ({ showModelOptions, isPopup, currentMode }: Mist
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,42 +1,54 @@
import { moonshotModels } from "@shared/api"
import { UpdateApiConfigurationRequestNew } from "@shared/proto/index.cline"
import { Mode } from "@shared/storage/types"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer, ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { modelsDevMoonshotModels } from "@shared/models/models-dev-catalog";
import { UpdateApiConfigurationRequestNew } from "@shared/proto/index.cline";
import type { Mode } from "@shared/storage/types";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ModelsServiceClient } from "@/services/grpc-client";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { DropdownContainer, ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
/**
* Props for the MoonshotProvider component
*/
interface MoonshotProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The Moonshot AI Studio provider configuration component
*/
export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: MoonshotProviderProps) => {
const { apiConfiguration } = useExtensionState()
export const MoonshotProvider = ({
showModelOptions,
isPopup,
currentMode,
}: MoonshotProviderProps) => {
const { apiConfiguration } = useExtensionState();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<DropdownContainer
className="dropdown-container"
style={{ position: "inherit" }}
>
<label htmlFor="moonshot-entrypoint">
<span style={{ fontWeight: 500, marginTop: 5 }}>Moonshot Entrypoint</span>
<span style={{ fontWeight: 500, marginTop: 5 }}>
Moonshot Entrypoint
</span>
</label>
<VSCodeDropdown
id="moonshot-entrypoint"
onChange={async (e) => {
const value = (e.target as any).value
const value = (e.target as EventTarget & { value: string }).value;
await ModelsServiceClient.updateApiConfiguration(
UpdateApiConfigurationRequestNew.create({
updates: {
@@ -46,13 +58,14 @@ export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: Moo
},
updateMask: ["options.moonshotApiLine"],
}),
)
);
}}
style={{
minWidth: 130,
position: "relative",
}}
value={apiConfiguration?.moonshotApiLine || "international"}>
value={apiConfiguration?.moonshotApiLine || "international"}
>
<VSCodeOption value="international">api.moonshot.ai</VSCodeOption>
<VSCodeOption value="china">api.moonshot.cn</VSCodeOption>
</VSCodeDropdown>
@@ -70,7 +83,7 @@ export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: Moo
},
updateMask: ["secrets.moonshotApiKey"],
}),
)
);
}}
providerName="Moonshot"
signupUrl={
@@ -84,9 +97,9 @@ export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: Moo
<>
<ModelSelector
label="Model"
models={moonshotModels}
onChange={async (e: any) => {
const value = e.target.value
models={modelsDevMoonshotModels}
onChange={async (e) => {
const value = e.target.value;
await ModelsServiceClient.updateApiConfiguration(
UpdateApiConfigurationRequestNew.create(
@@ -100,14 +113,18 @@ export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: Moo
updateMask: ["options.actModeApiModelId"],
},
),
)
);
}}
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,29 +1,37 @@
import { nebiusModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevNebiusModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the NebiusProvider component
*/
interface NebiusProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The Nebius AI Studio provider configuration component
*/
export const NebiusProvider = ({ showModelOptions, isPopup, currentMode }: NebiusProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const NebiusProvider = ({
showModelOptions,
isPopup,
currentMode,
}: NebiusProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -39,8 +47,8 @@ export const NebiusProvider = ({ showModelOptions, isPopup, currentMode }: Nebiu
<>
<ModelSelector
label="Model"
models={nebiusModels}
onChange={(e: any) =>
models={modelsDevNebiusModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -50,9 +58,13 @@ export const NebiusProvider = ({ showModelOptions, isPopup, currentMode }: Nebiu
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,30 +1,38 @@
import { nousResearchModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevNousResearchModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the NousResearchProvider component
*/
interface NousResearchProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The NousResearch provider configuration component
*/
export const NousResearchProvider = ({ showModelOptions, isPopup, currentMode }: NousResearchProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const NousResearchProvider = ({
showModelOptions,
isPopup,
currentMode,
}: NousResearchProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -38,10 +46,13 @@ export const NousResearchProvider = ({ showModelOptions, isPopup, currentMode }:
<>
<ModelSelector
label="Model"
models={nousResearchModels}
onChange={(e: any) =>
models={modelsDevNousResearchModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeNousResearchModelId", act: "actModeNousResearchModelId" },
{
plan: "planModeNousResearchModelId",
act: "actModeNousResearchModelId",
},
e.target.value,
currentMode,
)
@@ -49,21 +60,27 @@ export const NousResearchProvider = ({ showModelOptions, isPopup, currentMode }:
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
<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 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>
)
}
);
};
@@ -1,32 +1,46 @@
import { openAiNativeModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import ReasoningEffortSelector from "../ReasoningEffortSelector"
import { normalizeApiConfiguration, supportsReasoningEffortForModelId } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevOpenAiNativeModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import ReasoningEffortSelector from "../ReasoningEffortSelector";
import {
normalizeApiConfiguration,
supportsReasoningEffortForModelId,
} from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the OpenAINativeProvider component
*/
interface OpenAINativeProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The OpenAI (native) provider configuration component
*/
export const OpenAINativeProvider = ({ showModelOptions, isPopup, currentMode }: OpenAINativeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const OpenAINativeProvider = ({
showModelOptions,
isPopup,
currentMode,
}: OpenAINativeProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const showReasoningEffort = supportsReasoningEffortForModelId(selectedModelId, true)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
const showReasoningEffort = supportsReasoningEffortForModelId(
selectedModelId,
true,
);
return (
<div>
@@ -41,8 +55,8 @@ export const OpenAINativeProvider = ({ showModelOptions, isPopup, currentMode }:
<>
<ModelSelector
label="Model"
models={openAiNativeModels}
onChange={(e: any) =>
models={modelsDevOpenAiNativeModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -51,11 +65,17 @@ export const OpenAINativeProvider = ({ showModelOptions, isPopup, currentMode }:
}
selectedModelId={selectedModelId}
/>
{showReasoningEffort && <ReasoningEffortSelector currentMode={currentMode} />}
{showReasoningEffort && (
<ReasoningEffortSelector currentMode={currentMode} />
)}
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,30 +1,38 @@
import { sambanovaModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevSambanovaModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the SambanovaProvider component
*/
interface SambanovaProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
/**
* The SambaNova provider configuration component
*/
export const SambanovaProvider = ({ showModelOptions, isPopup, currentMode }: SambanovaProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const SambanovaProvider = ({
showModelOptions,
isPopup,
currentMode,
}: SambanovaProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -39,8 +47,8 @@ export const SambanovaProvider = ({ showModelOptions, isPopup, currentMode }: Sa
<>
<ModelSelector
label="Model"
models={sambanovaModels}
onChange={(e: any) =>
models={modelsDevSambanovaModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -50,9 +58,13 @@ export const SambanovaProvider = ({ showModelOptions, isPopup, currentMode }: Sa
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,23 +1,31 @@
import { wandbModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevWandbModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { ModelSelector } from "../common/ModelSelector";
import { normalizeApiConfiguration } from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
interface WandbProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
export const WandbProvider = ({ showModelOptions, isPopup, currentMode }: WandbProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const WandbProvider = ({
showModelOptions,
isPopup,
currentMode,
}: WandbProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
return (
<div>
@@ -33,8 +41,8 @@ export const WandbProvider = ({ showModelOptions, isPopup, currentMode }: WandbP
<>
<ModelSelector
label="Model"
models={wandbModels}
onChange={(e: any) =>
models={modelsDevWandbModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -44,9 +52,13 @@ export const WandbProvider = ({ showModelOptions, isPopup, currentMode }: WandbP
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -1,35 +1,52 @@
import { xaiModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer, ModelSelector } from "../common/ModelSelector"
import { getModeSpecificFields, normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { modelsDevXaiModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import {
VSCodeCheckbox,
VSCodeDropdown,
VSCodeOption,
} from "@vscode/webview-ui-toolkit/react";
import { useState } from "react";
import { useExtensionState } from "@/context/ExtensionStateContext";
import { DROPDOWN_Z_INDEX } from "../ApiOptions";
import { ApiKeyField } from "../common/ApiKeyField";
import { ModelInfoView } from "../common/ModelInfoView";
import { DropdownContainer, ModelSelector } from "../common/ModelSelector";
import {
getModeSpecificFields,
normalizeApiConfiguration,
} from "../utils/providerUtils";
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers";
/**
* Props for the XaiProvider component
*/
interface XaiProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
showModelOptions: boolean;
isPopup?: boolean;
currentMode: Mode;
}
export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
export const XaiProvider = ({
showModelOptions,
isPopup,
currentMode,
}: XaiProviderProps) => {
const { apiConfiguration } = useExtensionState();
const { handleFieldChange, handleModeFieldChange } =
useApiConfigurationHandlers();
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const modeFields = getModeSpecificFields(apiConfiguration, currentMode);
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(
apiConfiguration,
currentMode,
);
// Local state for reasoning effort toggle
const [reasoningEffortSelected, setReasoningEffortSelected] = useState(!!modeFields.reasoningEffort)
const [reasoningEffortSelected, setReasoningEffortSelected] = useState(
!!modeFields.reasoningEffort,
);
return (
<div>
@@ -45,10 +62,12 @@ export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProvi
fontSize: "12px",
marginTop: -10,
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 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>
@@ -57,8 +76,8 @@ export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProvi
<>
<ModelSelector
label="Model"
models={xaiModels}
onChange={(e: any) =>
models={modelsDevXaiModels}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
@@ -68,22 +87,28 @@ export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProvi
selectedModelId={selectedModelId}
/>
{selectedModelId && selectedModelId.includes("3-mini") && (
{selectedModelId?.includes("3-mini") && (
<>
<VSCodeCheckbox
checked={reasoningEffortSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setReasoningEffortSelected(isChecked)
onChange={(e) => {
const isChecked =
(e.target as EventTarget & { checked: boolean }).checked ===
true;
setReasoningEffortSelected(isChecked);
if (!isChecked) {
handleModeFieldChange(
{ plan: "planModeReasoningEffort", act: "actModeReasoningEffort" },
{
plan: "planModeReasoningEffort",
act: "actModeReasoningEffort",
},
"",
currentMode,
)
);
}
}}
style={{ marginTop: 0 }}>
style={{ marginTop: 0 }}
>
Modify reasoning effort
</VSCodeCheckbox>
@@ -92,18 +117,25 @@ export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProvi
<label htmlFor="reasoning-effort-dropdown">
<span style={{}}>Reasoning Effort</span>
</label>
<DropdownContainer className="dropdown-container" zIndex={DROPDOWN_Z_INDEX - 100}>
<DropdownContainer
className="dropdown-container"
zIndex={DROPDOWN_Z_INDEX - 100}
>
<VSCodeDropdown
id="reasoning-effort-dropdown"
onChange={(e: any) => {
onChange={(e) => {
handleModeFieldChange(
{ plan: "planModeReasoningEffort", act: "actModeReasoningEffort" },
e.target.value,
{
plan: "planModeReasoningEffort",
act: "actModeReasoningEffort",
},
(e.target as EventTarget & { value: string }).value,
currentMode,
)
);
}}
style={{ width: "100%", marginTop: 3 }}
value={modeFields.reasoningEffort || "high"}>
value={modeFields.reasoningEffort || "high"}
>
<VSCodeOption value="low">low</VSCodeOption>
<VSCodeOption value="high">high</VSCodeOption>
</VSCodeDropdown>
@@ -114,17 +146,23 @@ export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProvi
marginTop: 3,
marginBottom: 0,
color: "var(--vscode-descriptionForeground)",
}}>
High effort may produce more thorough analysis but takes longer and uses more tokens.
}}
>
High effort may produce more thorough analysis but takes
longer and uses more tokens.
</p>
</div>
)}
</>
)}
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
<ModelInfoView
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
</>
)}
</div>
)
}
);
};
@@ -9,6 +9,7 @@ import {
basetenModels,
bedrockDefaultModelId,
bedrockModels,
buildModelInfoNameMap,
cerebrasDefaultModelId,
cerebrasModels,
claudeCodeDefaultModelId,
@@ -73,6 +74,7 @@ import {
xaiDefaultModelId,
xaiModels,
} from "@shared/api";
import { getModelsDevProviderModels } from "@shared/models/models-dev-catalog";
import type { Mode } from "@shared/storage/types";
import * as reasoningSupport from "@shared/utils/reasoning-support";
@@ -96,25 +98,29 @@ export function getModelsForProvider(
basetenModels?: Record<string, ModelInfo>;
} = {},
): Record<string, ModelInfo> | undefined {
const modelsDevModels = getModelsDevProviderModels(provider);
const modelsDevOrFallback = (fallback: Record<string, ModelInfo>) =>
Object.keys(modelsDevModels).length > 0 ? modelsDevModels : fallback;
switch (provider) {
case "anthropic":
return anthropicModels;
return modelsDevOrFallback(anthropicModels);
case "claude-code":
return claudeCodeModels;
case "bedrock":
return bedrockModels;
return modelsDevOrFallback(bedrockModels);
case "vertex":
return vertexModels;
return modelsDevOrFallback(vertexModels);
case "gemini":
return geminiModels;
return modelsDevOrFallback(geminiModels);
case "openai-native":
return openAiNativeModels;
return modelsDevOrFallback(openAiNativeModels);
case "openai-codex":
return openAiCodexModels;
return modelsDevOrFallback(openAiCodexModels);
case "cline-pass":
return clinePassModels;
case "deepseek":
return deepSeekModels;
return modelsDevOrFallback(deepSeekModels);
case "qwen":
return apiConfiguration?.qwenApiLine === "china"
? mainlandQwenModels
@@ -122,59 +128,45 @@ export function getModelsForProvider(
case "qwen-code":
return qwenCodeModels;
case "doubao":
return doubaoModels;
return modelsDevOrFallback(doubaoModels);
case "mistral":
return mistralModels;
return modelsDevOrFallback(mistralModels);
case "asksage":
return askSageModels;
case "xai":
return xaiModels;
return modelsDevOrFallback(xaiModels);
case "moonshot":
return moonshotModels;
return modelsDevOrFallback(moonshotModels);
case "nebius":
return nebiusModels;
return modelsDevOrFallback(nebiusModels);
case "wandb":
return wandbModels;
return modelsDevOrFallback(wandbModels);
case "sambanova":
return sambanovaModels;
return modelsDevOrFallback(sambanovaModels);
case "cerebras":
return cerebrasModels;
return modelsDevOrFallback(cerebrasModels);
case "groq":
return groqModels;
return modelsDevOrFallback(groqModels);
case "baseten":
return dynamicModels?.basetenModels || basetenModels;
return dynamicModels?.basetenModels || modelsDevOrFallback(basetenModels);
case "sapaicore":
return sapAiCoreModels;
return modelsDevOrFallback(sapAiCoreModels);
case "huawei-cloud-maas":
return huaweiCloudMaasModels;
return modelsDevOrFallback(huaweiCloudMaasModels);
case "zai":
return apiConfiguration?.zaiApiLine === "china"
? mainlandZAiModels
: internationalZAiModels;
case "fireworks":
return fireworksModels;
return modelsDevOrFallback(fireworksModels);
case "minimax":
return minimaxModels;
return modelsDevOrFallback(minimaxModels);
case "huggingface":
return huggingFaceModels;
return modelsDevOrFallback(huggingFaceModels);
case "nousResearch":
return nousResearchModels;
return modelsDevOrFallback(nousResearchModels);
case "litellm":
return dynamicModels?.liteLlmModels;
// Providers with dynamic models - return undefined
case "openrouter":
case "cline":
case "openai":
case "ollama":
case "lmstudio":
case "vscode-lm":
case "requesty":
case "hicap":
case "dify":
case "vercel-ai-gateway":
case "oca":
case "aihubmix":
case "together":
default:
return undefined;
}
@@ -233,10 +225,29 @@ export function normalizeApiConfiguration(
selectedModelInfo,
};
};
const getModelsDevProviderData = (
providerId: ApiProvider,
fallbackModels: Record<string, ModelInfo>,
defaultId: string,
) => {
const modelsDevModels = getModelsDevProviderModels(providerId);
const models =
Object.keys(modelsDevModels).length > 0
? modelsDevModels
: fallbackModels;
const resolvedDefaultId = models[defaultId]
? defaultId
: (Object.keys(models)[0] ?? defaultId);
return getProviderData(models, resolvedDefaultId);
};
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId);
return getModelsDevProviderData(
"anthropic",
anthropicModels,
anthropicDefaultModelId,
);
case "claude-code":
return getProviderData(claudeCodeModels, claudeCodeDefaultModelId);
case "bedrock": {
@@ -259,18 +270,42 @@ export function normalizeApiConfiguration(
bedrockModels[bedrockDefaultModelId],
};
}
return getProviderData(bedrockModels, bedrockDefaultModelId);
return getModelsDevProviderData(
"bedrock",
bedrockModels,
bedrockDefaultModelId,
);
}
case "vertex":
return getProviderData(vertexModels, vertexDefaultModelId);
return getModelsDevProviderData(
"vertex",
vertexModels,
vertexDefaultModelId,
);
case "gemini":
return getProviderData(geminiModels, geminiDefaultModelId);
return getModelsDevProviderData(
"gemini",
geminiModels,
geminiDefaultModelId,
);
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId);
return getModelsDevProviderData(
"openai-native",
openAiNativeModels,
openAiNativeDefaultModelId,
);
case "openai-codex":
return getProviderData(openAiCodexModels, openAiCodexDefaultModelId);
return getModelsDevProviderData(
"openai-codex",
openAiCodexModels,
openAiCodexDefaultModelId,
);
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId);
return getModelsDevProviderData(
"deepseek",
deepSeekModels,
deepSeekDefaultModelId,
);
case "qwen": {
const qwenModels =
apiConfiguration?.qwenApiLine === "china"
@@ -285,9 +320,17 @@ export function normalizeApiConfiguration(
case "qwen-code":
return getProviderData(qwenCodeModels, qwenCodeDefaultModelId);
case "doubao":
return getProviderData(doubaoModels, doubaoDefaultModelId);
return getModelsDevProviderData(
"doubao",
doubaoModels,
doubaoDefaultModelId,
);
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId);
return getModelsDevProviderData(
"mistral",
mistralModels,
mistralDefaultModelId,
);
case "asksage":
return getProviderData(askSageModels, askSageDefaultModelId);
case "openrouter": {
@@ -360,17 +403,20 @@ export function normalizeApiConfiguration(
? configuredClinePassModelId
: clinePassDefaultModelId;
const clinePassModelInfo =
(currentMode === "plan"
currentMode === "plan"
? apiConfiguration?.planModeClinePassModelInfo
: apiConfiguration?.actModeClinePassModelInfo) ||
resolveClinePassModelInfo(
clinePassModelId,
options.clinePassModelInfoByName,
);
: apiConfiguration?.actModeClinePassModelInfo;
const clinePassModelInfoByName = clinePassModelInfo
? buildModelInfoNameMap({ [clinePassModelId]: clinePassModelInfo })
: options.clinePassModelInfoByName;
const resolvedClinePassModelInfo = resolveClinePassModelInfo(
clinePassModelId,
clinePassModelInfoByName,
);
return {
selectedProvider: provider,
selectedModelId: clinePassModelId,
selectedModelInfo: clinePassModelInfo,
selectedModelInfo: resolvedClinePassModelInfo,
};
}
case "openai": {
@@ -461,9 +507,13 @@ export function normalizeApiConfiguration(
};
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId);
return getModelsDevProviderData("xai", xaiModels, xaiDefaultModelId);
case "moonshot":
return getProviderData(moonshotModels, moonshotDefaultModelId);
return getModelsDevProviderData(
"moonshot",
moonshotModels,
moonshotDefaultModelId,
);
case "huggingface": {
const huggingFaceModelId =
currentMode === "plan"
@@ -473,21 +523,41 @@ export function normalizeApiConfiguration(
currentMode === "plan"
? apiConfiguration?.planModeHuggingFaceModelInfo
: apiConfiguration?.actModeHuggingFaceModelInfo;
const huggingFaceModelCatalog = getModelsDevProviderModels("huggingface");
return {
selectedProvider: provider,
selectedModelId: huggingFaceModelId || huggingFaceDefaultModelId,
selectedModelInfo:
huggingFaceModelInfo || huggingFaceModels[huggingFaceDefaultModelId],
huggingFaceModelInfo ||
huggingFaceModelCatalog[huggingFaceDefaultModelId] ||
Object.values(huggingFaceModelCatalog)[0] ||
huggingFaceModels[huggingFaceDefaultModelId],
};
}
case "nebius":
return getProviderData(nebiusModels, nebiusDefaultModelId);
return getModelsDevProviderData(
"nebius",
nebiusModels,
nebiusDefaultModelId,
);
case "wandb":
return getProviderData(wandbModels, wandbDefaultModelId);
return getModelsDevProviderData(
"wandb",
wandbModels,
wandbDefaultModelId,
);
case "sambanova":
return getProviderData(sambanovaModels, sambanovaDefaultModelId);
return getModelsDevProviderData(
"sambanova",
sambanovaModels,
sambanovaDefaultModelId,
);
case "cerebras":
return getProviderData(cerebrasModels, cerebrasDefaultModelId);
return getModelsDevProviderData(
"cerebras",
cerebrasModels,
cerebrasDefaultModelId,
);
case "groq": {
const groqModelId =
currentMode === "plan"
@@ -497,10 +567,15 @@ export function normalizeApiConfiguration(
currentMode === "plan"
? apiConfiguration?.planModeGroqModelInfo
: apiConfiguration?.actModeGroqModelInfo;
const groqModelCatalog = getModelsDevProviderModels("groq");
return {
selectedProvider: provider,
selectedModelId: groqModelId || groqDefaultModelId,
selectedModelInfo: groqModelInfo || groqModels[groqDefaultModelId],
selectedModelInfo:
groqModelInfo ||
groqModelCatalog[groqDefaultModelId] ||
Object.values(groqModelCatalog)[0] ||
groqModels[groqDefaultModelId],
};
}
case "baseten": {
@@ -513,10 +588,14 @@ export function normalizeApiConfiguration(
? apiConfiguration?.planModeBasetenModelInfo
: apiConfiguration?.actModeBasetenModelInfo;
const finalBasetenModelId = basetenModelId || basetenDefaultModelId;
const basetenModelCatalog = getModelsDevProviderModels("baseten");
return {
selectedProvider: provider,
selectedModelId: finalBasetenModelId,
selectedModelInfo: basetenModelInfo ||
basetenModelCatalog[finalBasetenModelId] ||
basetenModelCatalog[basetenDefaultModelId] ||
Object.values(basetenModelCatalog)[0] ||
basetenModels[finalBasetenModelId as keyof typeof basetenModels] ||
basetenModels[basetenDefaultModelId] || {
description: "Baseten model",
@@ -524,7 +603,11 @@ export function normalizeApiConfiguration(
};
}
case "sapaicore":
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId);
return getModelsDevProviderData(
"sapaicore",
sapAiCoreModels,
sapAiCoreDefaultModelId,
);
case "huawei-cloud-maas": {
const huaweiCloudMaasModelId =
currentMode === "plan"
@@ -534,12 +617,16 @@ export function normalizeApiConfiguration(
currentMode === "plan"
? apiConfiguration?.planModeHuaweiCloudMaasModelInfo
: apiConfiguration?.actModeHuaweiCloudMaasModelInfo;
const huaweiCloudMaasModelCatalog =
getModelsDevProviderModels("huawei-cloud-maas");
return {
selectedProvider: provider,
selectedModelId:
huaweiCloudMaasModelId || huaweiCloudMaasDefaultModelId,
selectedModelInfo:
huaweiCloudMaasModelInfo ||
huaweiCloudMaasModelCatalog[huaweiCloudMaasDefaultModelId] ||
Object.values(huaweiCloudMaasModelCatalog)[0] ||
huaweiCloudMaasModels[huaweiCloudMaasDefaultModelId],
};
}
@@ -590,13 +677,17 @@ export function normalizeApiConfiguration(
currentMode === "plan"
? apiConfiguration?.planModeFireworksModelId
: apiConfiguration?.actModeFireworksModelId;
const fireworksModelCatalog = getModelsDevProviderModels("fireworks");
return {
selectedProvider: provider,
selectedModelId: fireworksModelId || fireworksDefaultModelId,
selectedModelInfo:
fireworksModelId && fireworksModelId in fireworksModels
(fireworksModelId && fireworksModelCatalog[fireworksModelId]) ||
fireworksModelCatalog[fireworksDefaultModelId] ||
Object.values(fireworksModelCatalog)[0] ||
(fireworksModelId && fireworksModelId in fireworksModels
? fireworksModels[fireworksModelId as keyof typeof fireworksModels]
: fireworksModels[fireworksDefaultModelId],
: fireworksModels[fireworksDefaultModelId]),
};
}
case "oca": {
@@ -630,21 +721,31 @@ export function normalizeApiConfiguration(
};
}
case "minimax":
return getProviderData(minimaxModels, minimaxDefaultModelId);
return getModelsDevProviderData(
"minimax",
minimaxModels,
minimaxDefaultModelId,
);
case "nousResearch": {
const nousResearchModelId =
currentMode === "plan"
? apiConfiguration?.planModeNousResearchModelId
: apiConfiguration?.actModeNousResearchModelId;
const nousResearchModelCatalog =
getModelsDevProviderModels("nousResearch");
return {
selectedProvider: provider,
selectedModelId: nousResearchModelId || nousResearchDefaultModelId,
selectedModelInfo:
nousResearchModelId && nousResearchModelId in nousResearchModels
(nousResearchModelId &&
nousResearchModelCatalog[nousResearchModelId]) ||
nousResearchModelCatalog[nousResearchDefaultModelId] ||
Object.values(nousResearchModelCatalog)[0] ||
(nousResearchModelId && nousResearchModelId in nousResearchModels
? nousResearchModels[
nousResearchModelId as keyof typeof nousResearchModels
]
: nousResearchModels[nousResearchDefaultModelId],
: nousResearchModels[nousResearchDefaultModelId]),
};
}
default:
@@ -1085,26 +1186,6 @@ export async function syncModeConfigurations(
updates.actModeAihubmixModelInfo = sourceFields.aihubmixModelInfo;
break;
// Providers that use apiProvider + apiModelId fields
case "anthropic":
case "claude-code":
case "vertex":
case "gemini":
case "openai-native":
case "openai-codex":
case "deepseek":
case "qwen":
case "doubao":
case "mistral":
case "asksage":
case "xai":
case "nebius":
case "wandb":
case "sambanova":
case "cerebras":
case "sapaicore":
case "zai":
case "minimax":
default:
updates.planModeApiModelId = sourceFields.apiModelId;
updates.actModeApiModelId = sourceFields.apiModelId;
@@ -1120,7 +1201,7 @@ export { filterOpenRouterModelIds } from "@shared/utils/model-filters";
// Helper to get provider-specific configuration info and empty state guidance
export const getProviderInfo = (
provider: ApiProvider,
apiConfiguration: any,
apiConfiguration: ApiConfiguration,
effectiveMode: "plan" | "act",
): { modelId?: string; baseUrl?: string; helpText: string } => {
switch (provider) {
@@ -1,3 +0,0 @@
// Webview copies of feature-flag strings. Must match the extension's FeatureFlag
// enum, which can't be imported here (it pulls in Node-only deps).
export const CLINE_PASS_FEATURE_FLAG = "ext-cline-pass"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
const DEFAULT_CLINE_APP_BASE_URL = "https://app.cline.bot"
const CLINE_PASS_SUBSCRIPTION_PATH = "dashboard/subscription"
export function buildClinePassSubscriptionUrl(appBaseUrl?: string): string {
try {
const baseUrl = appBaseUrl || DEFAULT_CLINE_APP_BASE_URL
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`
const url = new URL(CLINE_PASS_SUBSCRIPTION_PATH, base)
url.searchParams.set("personal", "true")
return url.toString()
} catch {
return `${DEFAULT_CLINE_APP_BASE_URL}/${CLINE_PASS_SUBSCRIPTION_PATH}?personal=true`
}
}
@@ -1,6 +1,6 @@
import type { ApiConfiguration, ApiProvider } from "@shared/api"
import PROVIDERS from "@shared/providers/providers.json"
import type { RemoteConfigFields } from "@shared/storage/state-keys"
import type { ApiConfiguration, ApiProvider } from "@shared/api";
import { modelsDevProviderOptions } from "@shared/models/models-dev-catalog";
import type { RemoteConfigFields } from "@shared/storage/state-keys";
/**
* Returns a list of API providers that are configured (have required credentials/settings)
@@ -11,114 +11,114 @@ export function getConfiguredProviders(
apiConfiguration: ApiConfiguration | undefined,
): ApiProvider[] {
if (remoteConfig?.remoteConfiguredProviders?.length) {
return remoteConfig.remoteConfiguredProviders
return remoteConfig.remoteConfiguredProviders;
}
const configured: ApiProvider[] = []
const configured: ApiProvider[] = [];
if (!apiConfiguration) {
return ["cline"] // Cline is always available
return ["cline"]; // Cline is always available
}
// Cline - always available (uses account-based auth)
configured.push("cline")
configured.push("cline");
// Anthropic - requires API key
if (apiConfiguration.apiKey) {
configured.push("anthropic")
configured.push("anthropic");
}
// OpenRouter - requires API key
if (apiConfiguration.openRouterApiKey) {
configured.push("openrouter")
configured.push("openrouter");
}
// Bedrock - requires region
if (apiConfiguration.awsRegion) {
configured.push("bedrock")
configured.push("bedrock");
}
// Vertex - requires project ID and region
if (apiConfiguration.vertexProjectId && apiConfiguration.vertexRegion) {
configured.push("vertex")
configured.push("vertex");
}
// Gemini - requires API key
if (apiConfiguration.geminiApiKey) {
configured.push("gemini")
configured.push("gemini");
}
// OpenAI Native - requires API key
if (apiConfiguration.openAiNativeApiKey) {
configured.push("openai-native")
configured.push("openai-native");
}
// OpenAI Codex - subscription-based OAuth, always available
configured.push("openai-codex")
configured.push("openai-codex");
// DeepSeek - requires API key
if (apiConfiguration.deepSeekApiKey) {
configured.push("deepseek")
configured.push("deepseek");
}
// xAI - requires API key
if (apiConfiguration.xaiApiKey) {
configured.push("xai")
configured.push("xai");
}
// Qwen - requires API key
if (apiConfiguration.qwenApiKey) {
configured.push("qwen")
configured.push("qwen");
}
// Doubao - requires API key
if (apiConfiguration.doubaoApiKey) {
configured.push("doubao")
configured.push("doubao");
}
// Mistral - requires API key
if (apiConfiguration.mistralApiKey) {
configured.push("mistral")
configured.push("mistral");
}
// Requesty - requires API key
if (apiConfiguration.requestyApiKey) {
configured.push("requesty")
configured.push("requesty");
}
// Fireworks - requires API key
if (apiConfiguration.fireworksApiKey) {
configured.push("fireworks")
configured.push("fireworks");
}
// Together - requires API key
if (apiConfiguration.togetherApiKey) {
configured.push("together")
configured.push("together");
}
// Moonshot - requires API key
if (apiConfiguration.moonshotApiKey) {
configured.push("moonshot")
configured.push("moonshot");
}
// Nebius - requires API key
if (apiConfiguration.nebiusApiKey) {
configured.push("nebius")
configured.push("nebius");
}
// AskSage - requires API key
if (apiConfiguration.asksageApiKey) {
configured.push("asksage")
configured.push("asksage");
}
// SambaNova - requires API key
if (apiConfiguration.sambanovaApiKey) {
configured.push("sambanova")
configured.push("sambanova");
}
// Cerebras - requires API key
if (apiConfiguration.cerebrasApiKey) {
configured.push("cerebras")
configured.push("cerebras");
}
// SAP AI Core - requires base URL, client ID, client secret, and token URL
@@ -128,62 +128,62 @@ export function getConfiguredProviders(
apiConfiguration.sapAiCoreClientSecret &&
apiConfiguration.sapAiCoreTokenUrl
) {
configured.push("sapaicore")
configured.push("sapaicore");
}
// Z AI - requires API key
if (apiConfiguration.zaiApiKey) {
configured.push("zai")
configured.push("zai");
}
// Groq - requires API key
if (apiConfiguration.groqApiKey) {
configured.push("groq")
configured.push("groq");
}
// Hugging Face - requires API key
if (apiConfiguration.huggingFaceApiKey) {
configured.push("huggingface")
configured.push("huggingface");
}
// Baseten - requires API key
if (apiConfiguration.basetenApiKey) {
configured.push("baseten")
configured.push("baseten");
}
// Dify - requires base URL and API key
if (apiConfiguration.difyBaseUrl && apiConfiguration.difyApiKey) {
configured.push("dify")
configured.push("dify");
}
// Minimax - requires API key
if (apiConfiguration.minimaxApiKey) {
configured.push("minimax")
configured.push("minimax");
}
// Hicap - requires API key
if (apiConfiguration.hicapApiKey) {
configured.push("hicap")
configured.push("hicap");
}
// Huawei Cloud MaaS - requires API key
if (apiConfiguration.huaweiCloudMaasApiKey) {
configured.push("huawei-cloud-maas")
configured.push("huawei-cloud-maas");
}
// Vercel AI Gateway - requires API key
if (apiConfiguration.vercelAiGatewayApiKey) {
configured.push("vercel-ai-gateway")
configured.push("vercel-ai-gateway");
}
// AIHubMix - requires API key
if (apiConfiguration.aihubmixApiKey) {
configured.push("aihubmix")
configured.push("aihubmix");
}
// NousResearch - requires API key
if (apiConfiguration.nousResearchApiKey) {
configured.push("nousResearch")
configured.push("nousResearch");
}
// OpenAI Compatible - requires base URL and API key, OR has model configured
@@ -192,17 +192,25 @@ export function getConfiguredProviders(
apiConfiguration.planModeOpenAiModelId ||
apiConfiguration.actModeOpenAiModelId
) {
configured.push("openai")
configured.push("openai");
}
// Ollama - local provider, check base URL OR model configured
if (apiConfiguration.ollamaBaseUrl || apiConfiguration.planModeOllamaModelId || apiConfiguration.actModeOllamaModelId) {
configured.push("ollama")
if (
apiConfiguration.ollamaBaseUrl ||
apiConfiguration.planModeOllamaModelId ||
apiConfiguration.actModeOllamaModelId
) {
configured.push("ollama");
}
// LM Studio - local provider, check base URL OR model configured
if (apiConfiguration.lmStudioBaseUrl || apiConfiguration.planModeLmStudioModelId || apiConfiguration.actModeLmStudioModelId) {
configured.push("lmstudio")
if (
apiConfiguration.lmStudioBaseUrl ||
apiConfiguration.planModeLmStudioModelId ||
apiConfiguration.actModeLmStudioModelId
) {
configured.push("lmstudio");
}
// LiteLLM - check base URL, API key OR model configured
@@ -212,35 +220,37 @@ export function getConfiguredProviders(
apiConfiguration.planModeLiteLlmModelId ||
apiConfiguration.actModeLiteLlmModelId
) {
configured.push("litellm")
configured.push("litellm");
}
// VSCode LM - always potentially available
configured.push("vscode-lm")
configured.push("vscode-lm");
// Claude Code - requires path
if (apiConfiguration.claudeCodePath) {
configured.push("claude-code")
configured.push("claude-code");
}
// Qwen Code - requires API key (same as Qwen)
if (apiConfiguration.qwenApiKey) {
configured.push("qwen-code")
configured.push("qwen-code");
}
// OCA - requires base URL
if (apiConfiguration.ocaBaseUrl) {
configured.push("oca")
configured.push("oca");
}
return configured
return configured;
}
/**
* Get provider display label from provider value
* Uses the canonical providers.json as source of truth
* Uses the generated models.dev provider catalog as source of truth
*/
export function getProviderLabel(provider: ApiProvider): string {
const providerEntry = PROVIDERS.list.find((p) => p.value === provider)
return providerEntry?.label || provider
const providerEntry = modelsDevProviderOptions.find(
(p) => p.value === provider,
);
return providerEntry?.label || provider;
}
+150 -119
View File
@@ -1,10 +1,10 @@
/// <reference types="vitest/config" />
import { writeFileSync } from "node:fs"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react-swc"
import { resolve } from "path"
import { defineConfig, type Plugin, ViteDevServer } from "vite"
import { writeFileSync } from "node:fs";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react-swc";
import { resolve } from "path";
import { defineConfig, loadEnv, type Plugin, type ViteDevServer } from "vite";
// Custom plugin to write the server port to a file
const writePortToFile = (): Plugin => {
@@ -12,135 +12,166 @@ const writePortToFile = (): Plugin => {
name: "write-port-to-file",
configureServer(server: ViteDevServer) {
server.httpServer?.once("listening", () => {
const address = server.httpServer?.address()
const port = typeof address === "object" && address ? address.port : null
const address = server.httpServer?.address();
const port =
typeof address === "object" && address ? address.port : null;
if (port) {
const portFilePath = resolve(__dirname, ".vite-port")
writeFileSync(portFilePath, port.toString())
const portFilePath = resolve(__dirname, ".vite-port");
writeFileSync(portFilePath, port.toString());
} else {
console.warn("[writePortToFile] Could not determine server port")
console.warn("[writePortToFile] Could not determine server port");
}
})
});
},
}
}
};
};
const isDevBuild = process.argv.includes("--dev-build")
const isDevBuild = process.argv.includes("--dev-build");
// Valid platforms, these should the keys in platform-configs.json
const VALID_PLATFORMS = ["vscode", "standalone"]
const platform = process.env.PLATFORM || "vscode" // Default to vscode
const VALID_PLATFORMS = ["vscode", "standalone"];
if (!VALID_PLATFORMS.includes(platform)) {
throw new Error(`Invalid PLATFORM "${platform}". Must be one of: ${VALID_PLATFORMS.join(", ")}`)
}
console.log("Building webview for", platform)
export default defineConfig(({ mode }) => {
const env = {
...loadEnv(mode, __dirname, ""),
...process.env,
};
export default defineConfig({
base: "./",
optimizeDeps: {
force: true, // Forces re-optimization
},
plugins: [react(), tailwindcss(), writePortToFile()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/setupTests.ts"],
coverage: {
provider: "v8",
reportOnFailure: true,
reporter: ["html", "lcov", "text"],
reportsDirectory: "./coverage",
exclude: [
"**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}",
const platform = env.PLATFORM || "vscode"; // Default to vscode
"**/*.d.ts",
"**/vite-env.d.ts",
"**/*.{config,setup}.{js,ts,mjs,cjs}",
if (!VALID_PLATFORMS.includes(platform)) {
throw new Error(
`Invalid PLATFORM "${platform}". Must be one of: ${VALID_PLATFORMS.join(", ")}`,
);
}
console.log("Building webview for", platform);
"**/*.{css,scss,sass,less,styl}",
"**/*.{svg,png,jpg,jpeg,gif,ico}",
"**/*.{json,yaml,yml}",
"**/__mocks__/**",
"node_modules/**",
"build/**",
"coverage/**",
"dist/**",
"public/**",
"src/services/grpc-client.ts",
],
return {
base: "./",
optimizeDeps: {
force: true, // Forces re-optimization
},
},
build: {
outDir: "build",
reportCompressedSize: false,
// Only minify in production build
minify: !isDevBuild,
// Enable inline source maps for dev build
sourcemap: isDevBuild ? "inline" : false,
rollupOptions: {
output: {
inlineDynamicImports: true,
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`,
// Disable compact output for dev build
compact: !isDevBuild,
// Add generous formatting for dev build
...(isDevBuild && {
generatedCode: {
constBindings: false,
objectShorthand: false,
arrowFunctions: false,
},
}),
plugins: [react(), tailwindcss(), writePortToFile()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/setupTests.ts"],
coverage: {
provider: "v8",
reportOnFailure: true,
reporter: ["html", "lcov", "text"],
reportsDirectory: "./coverage",
exclude: [
"**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}",
"**/*.d.ts",
"**/vite-env.d.ts",
"**/*.{config,setup}.{js,ts,mjs,cjs}",
"**/*.{css,scss,sass,less,styl}",
"**/*.{svg,png,jpg,jpeg,gif,ico}",
"**/*.{json,yaml,yml}",
"**/__mocks__/**",
"node_modules/**",
"build/**",
"coverage/**",
"dist/**",
"public/**",
"src/services/grpc-client.ts",
],
},
},
chunkSizeWarningLimit: 100000,
},
server: {
port: 25463,
hmr: {
host: "localhost",
protocol: "ws",
build: {
outDir: "build",
reportCompressedSize: false,
// Only minify in production build
minify: !isDevBuild,
// Enable inline source maps for dev build
sourcemap: isDevBuild ? "inline" : false,
rollupOptions: {
output: {
inlineDynamicImports: true,
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`,
// Disable compact output for dev build
compact: !isDevBuild,
// Add generous formatting for dev build
...(isDevBuild && {
generatedCode: {
constBindings: false,
objectShorthand: false,
arrowFunctions: false,
},
}),
},
},
chunkSizeWarningLimit: 100000,
},
cors: {
origin: "*",
methods: "*",
allowedHeaders: "*",
server: {
port: 25463,
hmr: {
host: "localhost",
protocol: "ws",
},
cors: {
origin: "*",
methods: "*",
allowedHeaders: "*",
},
},
},
define: {
__PLATFORM__: JSON.stringify(platform),
__NODE_PLATFORM__: JSON.stringify(process.platform),
"process.env.CLINE_ENVIRONMENT": JSON.stringify(process.env.CLINE_ENVIRONMENT ?? "production"),
"process.env.IS_DEV": JSON.stringify(process.env.IS_DEV),
"process.env.IS_TEST": JSON.stringify(process.env.IS_TEST),
"process.env.CI": JSON.stringify(process.env.CI),
// PostHog environment variables
"process.env.TELEMETRY_SERVICE_API_KEY": JSON.stringify(process.env.TELEMETRY_SERVICE_API_KEY),
"process.env.ERROR_SERVICE_API_KEY": JSON.stringify(process.env.ERROR_SERVICE_API_KEY),
"process.env.ENABLE_ERROR_AUTOCAPTURE": JSON.stringify(process.env.ENABLE_ERROR_AUTOCAPTURE),
// OpenTelemetry environment variables
"process.env.OTEL_TELEMETRY_ENABLED": JSON.stringify(process.env.OTEL_TELEMETRY_ENABLED),
"process.env.OTEL_METRICS_EXPORTER": JSON.stringify(process.env.OTEL_METRICS_EXPORTER),
"process.env.OTEL_LOGS_EXPORTER": JSON.stringify(process.env.OTEL_LOGS_EXPORTER),
"process.env.OTEL_EXPORTER_OTLP_PROTOCOL": JSON.stringify(process.env.OTEL_EXPORTER_OTLP_PROTOCOL),
"process.env.OTEL_EXPORTER_OTLP_ENDPOINT": JSON.stringify(process.env.OTEL_EXPORTER_OTLP_ENDPOINT),
"process.env.OTEL_EXPORTER_OTLP_HEADERS": JSON.stringify(process.env.OTEL_EXPORTER_OTLP_HEADERS),
"process.env.OTEL_METRIC_EXPORT_INTERVAL": JSON.stringify(process.env.OTEL_METRIC_EXPORT_INTERVAL),
},
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
"@components": resolve(__dirname, "./src/components"),
"@context": resolve(__dirname, "./src/context"),
"@shared": resolve(__dirname, "../src/shared"),
"@utils": resolve(__dirname, "./src/utils"),
define: {
__PLATFORM__: JSON.stringify(platform),
__NODE_PLATFORM__: JSON.stringify(process.platform),
"process.env.CLINE_ENVIRONMENT": JSON.stringify(
env.CLINE_ENVIRONMENT ?? "production",
),
"process.env.IS_DEV": JSON.stringify(env.IS_DEV),
"process.env.IS_TEST": JSON.stringify(env.IS_TEST),
"process.env.CI": JSON.stringify(env.CI),
// PostHog environment variables
"process.env.TELEMETRY_SERVICE_API_KEY": JSON.stringify(
env.TELEMETRY_SERVICE_API_KEY,
),
"process.env.ERROR_SERVICE_API_KEY": JSON.stringify(
env.ERROR_SERVICE_API_KEY,
),
"process.env.ENABLE_ERROR_AUTOCAPTURE": JSON.stringify(
env.ENABLE_ERROR_AUTOCAPTURE,
),
// OpenTelemetry environment variables
"process.env.OTEL_TELEMETRY_ENABLED": JSON.stringify(
env.OTEL_TELEMETRY_ENABLED,
),
"process.env.OTEL_METRICS_EXPORTER": JSON.stringify(
env.OTEL_METRICS_EXPORTER,
),
"process.env.OTEL_LOGS_EXPORTER": JSON.stringify(env.OTEL_LOGS_EXPORTER),
"process.env.OTEL_EXPORTER_OTLP_PROTOCOL": JSON.stringify(
env.OTEL_EXPORTER_OTLP_PROTOCOL,
),
"process.env.OTEL_EXPORTER_OTLP_ENDPOINT": JSON.stringify(
env.OTEL_EXPORTER_OTLP_ENDPOINT,
),
"process.env.OTEL_EXPORTER_OTLP_HEADERS": JSON.stringify(
env.OTEL_EXPORTER_OTLP_HEADERS,
),
"process.env.OTEL_METRIC_EXPORT_INTERVAL": JSON.stringify(
env.OTEL_METRIC_EXPORT_INTERVAL,
),
},
},
})
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
"@components": resolve(__dirname, "./src/components"),
"@context": resolve(__dirname, "./src/context"),
"@shared": resolve(__dirname, "../src/shared"),
"@utils": resolve(__dirname, "./src/utils"),
},
},
};
});
@@ -31,7 +31,9 @@ const PROVIDER_IDS_MAP: ReadonlyArray<{
generatedProviderId: "deepseek",
runtimeProviderId: "deepseek",
},
{ modelsDevKey: "doubao", generatedProviderId: "doubao" },
{ modelsDevKey: "xai", generatedProviderId: "xai" },
{ modelsDevKey: "mistral", generatedProviderId: "mistral" },
{
modelsDevKey: "togetherai",
generatedProviderId: "together",
@@ -98,7 +100,11 @@ const PROVIDER_IDS_MAP: ReadonlyArray<{
runtimeProviderId: "aihubmix",
},
{ modelsDevKey: "hicap", runtimeProviderId: "hicap" },
{ modelsDevKey: "nous-research", runtimeProviderId: "nousResearch" },
{
modelsDevKey: "nous-research",
generatedProviderId: "nousResearch",
runtimeProviderId: "nousResearch",
},
{ modelsDevKey: "huawei-cloud-maas", runtimeProviderId: "huawei-cloud-maas" },
{
modelsDevKey: "baseten",