Compare commits

...

9 Commits

Author SHA1 Message Date
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
28 changed files with 1025 additions and 199 deletions
+20
View File
@@ -1,5 +1,25 @@
# Changelog
## [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
+1 -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.2",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+13 -2
View File
@@ -1,4 +1,11 @@
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"
@@ -204,6 +211,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 +300,10 @@ 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" })
})
})
+2 -1
View File
@@ -11,6 +11,7 @@ import { McpHub } from "@services/mcp/McpHub"
import type { ApiProvider, ModelInfo } from "@shared/api"
import type { ChatContent } from "@shared/ChatContent"
import type { ExtensionState, Platform } from "@shared/ExtensionMessage"
import { DEFAULT_FOCUS_CHAIN_SETTINGS } from "@shared/FocusChainSettings"
import type { HistoryItem } from "@shared/HistoryItem"
import type { McpMarketplaceCatalog, McpMarketplaceItem } from "@shared/mcp"
import { type Settings } from "@shared/storage/state-keys"
@@ -856,7 +857,7 @@ export class Controller {
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
const browserSettings = this.stateManager.getGlobalSettingsKey("browserSettings")
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings") ?? DEFAULT_FOCUS_CHAIN_SETTINGS
const preferredLanguage = this.stateManager.getGlobalSettingsKey("preferredLanguage")
const mode = this.stateManager.getGlobalSettingsKey("mode")
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
@@ -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");
}
}
@@ -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,
@@ -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":
@@ -2,8 +2,12 @@ import { AskResponseRequest } from "@shared/proto/cline/task"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useMemo, useState } from "react"
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
import { CLINE_PASS_FEATURE_FLAG } from "@/constants/featureFlags"
import { useClineAuth } from "@/context/ClineAuthContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useHasFeatureFlag } from "@/hooks/useFeatureFlag"
import { AccountServiceClient, TaskServiceClient } from "@/services/grpc-client"
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
interface CreditLimitErrorProps {
currentBalance: number
@@ -26,7 +30,12 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
totalSpent,
}) => {
const { activeOrganization } = useClineAuth()
const { mode, navigateToSettings } = useExtensionState()
const { handleModeFieldChange } = useApiConfigurationHandlers()
const isClinePassEnabled = useHasFeatureFlag(CLINE_PASS_FEATURE_FLAG)
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 +57,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 +88,26 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
</div>
</div>
{isClinePassEnabled && (
<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,23 +2,29 @@ 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"
import CreateWorktreeModal from "@/components/worktrees/CreateWorktreeModal"
import { CLINE_PASS_FEATURE_FLAG } from "@/constants/featureFlags"
import { useClineAuth } from "@/context/ClineAuthContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useHasFeatureFlag } from "@/hooks/useFeatureFlag"
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
@@ -58,6 +64,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
}, [])
const { clineUser } = useClineAuth()
const isClinePassEnabled = useHasFeatureFlag(CLINE_PASS_FEATURE_FLAG)
const {
openRouterModels,
navigateToSettings,
@@ -68,6 +75,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 +218,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 +235,67 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
}
}, [])
const clinePassPromoBanner = useMemo((): BannerData | undefined => {
if (
!isClinePassEnabled ||
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,
isClinePassEnabled,
navigateToSettings,
])
/**
* Build array of active banners for carousel
* Combines hardcoded banners (bannerConfig) with dynamic banners from extension state
@@ -247,8 +318,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 +332,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 +385,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 ? (
@@ -21,7 +21,6 @@ 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"
@@ -157,15 +156,24 @@ const ApiOptions = ({
// Filter by remote config if remoteConfiguredProviders is set
const remoteProviders: string[] = remoteConfigSettings?.remoteConfiguredProviders || []
if (remoteProviders.length > 0) {
providers = providers.filter((option) => remoteProviders.includes(option.value))
const effectiveRemoteProviders =
isClinePassEnabled && remoteProviders.includes("cline-pass") && !remoteProviders.includes("cline")
? [...remoteProviders, "cline"]
: remoteProviders
providers = providers.filter((option) => effectiveRemoteProviders.includes(option.value))
}
return providers
}, [isClinePassEnabled, remoteConfigSettings])
const getProviderDisplayLabel = useCallback((option: (typeof PROVIDERS.list)[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(() => {
@@ -177,13 +185,19 @@ const ApiOptions = ({
const searchableItems = useMemo(() => {
return providerOptions.map((option) => ({
value: option.value,
html: option.label,
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,
}))
}, [providerOptions])
}, [getProviderDisplayLabel, providerOptions])
const fuse = useMemo(() => {
return new Fuse(searchableItems, {
keys: ["html"],
keys: ["html", "searchText"],
threshold: 0.3,
shouldSort: true,
isCaseSensitive: false,
@@ -366,20 +380,17 @@ const ApiOptions = ({
<HicapProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
{apiConfiguration && selectedProvider === "cline" && (
{apiConfiguration && (selectedProvider === "cline" || (isClinePassEnabled && selectedProvider === "cline-pass")) && (
<ClineProvider
currentMode={currentMode}
initialModelTab={initialModelTab}
isClinePassEnabled={isClinePassEnabled}
isPopup={isPopup}
selectedProvider={selectedProvider}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && isClinePassEnabled && selectedProvider === "cline-pass" && (
<ClinePassProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
{apiConfiguration && selectedProvider === "asksage" && (
<AskSageProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
@@ -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>
@@ -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;
`
@@ -4,9 +4,16 @@ 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
*/
@@ -25,6 +32,7 @@ export const DeepSeekProvider = ({ showModelOptions, isPopup, currentMode }: Dee
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const showReasoningEffort = DEEPSEEK_REASONING_EFFORT_MODELS.has(selectedModelId)
return (
<div>
@@ -50,6 +58,8 @@ export const DeepSeekProvider = ({ showModelOptions, isPopup, currentMode }: Dee
selectedModelId={selectedModelId}
/>
{showReasoningEffort && <ReasoningEffortSelector currentMode={currentMode} />}
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
</>
)}
@@ -9,6 +9,7 @@ import {
basetenModels,
bedrockDefaultModelId,
bedrockModels,
buildModelInfoNameMap,
cerebrasDefaultModelId,
cerebrasModels,
claudeCodeDefaultModelId,
@@ -360,17 +361,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": {
@@ -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`
}
}
+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"),
},
},
};
});