mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
30 Commits
cli-v3.0.41
...
v4.0.7
| Author | SHA1 | Date | |
|---|---|---|---|
| e867f4520b | |||
| a2ca96cae1 | |||
| edc088fe5f | |||
| 720cf5ff3f | |||
| 3037b393a8 | |||
| 7eb7897ff2 | |||
| 797565f203 | |||
| 3d312a4bb8 | |||
| aa3f38d1d9 | |||
| 302967f679 | |||
| 8c859c182c | |||
| 83e062e743 | |||
| 264e2c9835 | |||
| 81a6fba11c | |||
| 9da3c59553 | |||
| 0e01b99e76 | |||
| ba5f22f58c | |||
| 1cbfc7197e | |||
| 4d8f9ea1a3 | |||
| 40b0b0c236 | |||
| 8c5e1289bf | |||
| 4cddde58ed | |||
| 9d45accc3b | |||
| 57760c1e20 | |||
| fc33e8dbd9 | |||
| b1dcaf576a | |||
| 419f2cea73 | |||
| 54d8c695a3 | |||
| 5a62ab8564 | |||
| f81afb51b5 |
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## [4.0.7]
|
||||
|
||||
### Added
|
||||
|
||||
- Add a ClinePass limit-reached error with a one-click option to switch to Cline usage-based billing.
|
||||
- Allow selecting Cline free models on the ClinePass provider, organized into Subscribed and Free tabs with model descriptions.
|
||||
|
||||
### Changed
|
||||
|
||||
- Refine ClinePass onboarding and provider settings copy, and open the "learn more" link via the in-app URL handler.
|
||||
- Remove the Cline model picker recommendation copy.
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove all references to GLM 5.1.
|
||||
|
||||
## [4.0.6]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Generalize the model capability warning so it applies more broadly.
|
||||
|
||||
## [4.0.5]
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for Claude Sonnet 5 across the Anthropic, Bedrock, Vertex, Claude Code, SAP AI Core, OpenRouter, and Vercel AI Gateway providers, including model picker and recommended-model updates.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -125,12 +125,12 @@ describe("buildConnectorStartRequest", () => {
|
||||
io: { writeln: vi.fn(), writeErr: vi.fn() },
|
||||
loggerConfig: { enabled: false, level: "info", destination: "stdout" },
|
||||
systemRules: "Rules",
|
||||
defaultModel: "cline-pass/glm-5.1",
|
||||
defaultModel: "cline-pass/glm-5.2",
|
||||
});
|
||||
|
||||
expect(request.provider).toBe("cline-pass");
|
||||
expect(request.apiKey).toBe("workos:resolved-token");
|
||||
expect(request.model).toBe("cline-pass/glm-5.1");
|
||||
expect(request.model).toBe("cline-pass/glm-5.2");
|
||||
});
|
||||
|
||||
it("uses auth material resolved by provider settings manager", async () => {
|
||||
@@ -153,11 +153,11 @@ describe("buildConnectorStartRequest", () => {
|
||||
io: { writeln: vi.fn(), writeErr: vi.fn() },
|
||||
loggerConfig: { enabled: false, level: "info", destination: "stdout" },
|
||||
systemRules: "Rules",
|
||||
defaultModel: "cline-pass/glm-5.1",
|
||||
defaultModel: "cline-pass/glm-5.2",
|
||||
});
|
||||
|
||||
expect(request.provider).toBe("cline-pass");
|
||||
expect(request.apiKey).toBe("workos:resolved-token");
|
||||
expect(request.model).toBe("cline-pass/glm-5.1");
|
||||
expect(request.model).toBe("cline-pass/glm-5.2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.7",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import "should"
|
||||
import {
|
||||
type ApiConfiguration,
|
||||
clinePassDefaultModelId,
|
||||
clinePassModelInfoSaneDefaults,
|
||||
clinePassModels,
|
||||
type ModelInfo,
|
||||
} from "@shared/api"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import sinon from "sinon"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildApiHandler } from "../index"
|
||||
|
||||
describe("buildApiHandler", () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(ClineAccountService, "getInstance").returns({} as any)
|
||||
sinon.stub(AuthService, "getInstance").returns({} as any)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const buildClinePassHandler = (configuration: Partial<ApiConfiguration>, mode: Mode = "act") =>
|
||||
buildApiHandler(
|
||||
{
|
||||
planModeApiProvider: "cline-pass",
|
||||
actModeApiProvider: "cline-pass",
|
||||
...configuration,
|
||||
} as ApiConfiguration,
|
||||
mode,
|
||||
)
|
||||
|
||||
describe("cline-pass provider", () => {
|
||||
const freeModelInfo: ModelInfo = {
|
||||
...clinePassModelInfoSaneDefaults,
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 256_000,
|
||||
description: "A free model",
|
||||
}
|
||||
|
||||
it("passes a free (non cline-pass prefixed) model id through with its stored info", () => {
|
||||
const handler = buildClinePassHandler({
|
||||
actModeClinePassModelId: "kwaipilot/kat-coder-pro",
|
||||
actModeClinePassModelInfo: freeModelInfo,
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("kwaipilot/kat-coder-pro")
|
||||
model.info.should.deepEqual(freeModelInfo)
|
||||
})
|
||||
|
||||
it("passes a :free suffixed model id through with its stored info", () => {
|
||||
const handler = buildClinePassHandler({
|
||||
actModeClinePassModelId: "arcee-ai/trinity-large-preview:free",
|
||||
actModeClinePassModelInfo: freeModelInfo,
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("arcee-ai/trinity-large-preview:free")
|
||||
model.info.should.deepEqual(freeModelInfo)
|
||||
})
|
||||
|
||||
it("falls back to sane defaults for a free model id without stored info", () => {
|
||||
const handler = buildClinePassHandler({
|
||||
actModeClinePassModelId: "kwaipilot/kat-coder-pro",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("kwaipilot/kat-coder-pro")
|
||||
model.info.should.deepEqual(clinePassModelInfoSaneDefaults)
|
||||
})
|
||||
|
||||
it("resolves cline-pass ids against the static model table", () => {
|
||||
const handler = buildClinePassHandler({
|
||||
actModeClinePassModelId: "cline-pass/glm-5.2",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("cline-pass/glm-5.2")
|
||||
model.info.should.deepEqual(clinePassModels["cline-pass/glm-5.2"])
|
||||
})
|
||||
|
||||
it("falls back to the default pass model when no model id is configured", () => {
|
||||
const handler = buildClinePassHandler({})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal(clinePassDefaultModelId)
|
||||
model.info.should.deepEqual(clinePassModels[clinePassDefaultModelId])
|
||||
})
|
||||
|
||||
it("resolves plan and act mode model ids independently", () => {
|
||||
const configuration: Partial<ApiConfiguration> = {
|
||||
planModeClinePassModelId: "kwaipilot/kat-coder-pro",
|
||||
planModeClinePassModelInfo: freeModelInfo,
|
||||
actModeClinePassModelId: "cline-pass/glm-5.2",
|
||||
}
|
||||
|
||||
buildClinePassHandler(configuration, "plan").getModel().id.should.equal("kwaipilot/kat-coder-pro")
|
||||
buildClinePassHandler(configuration, "act").getModel().id.should.equal("cline-pass/glm-5.2")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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({
|
||||
@@ -289,10 +292,15 @@ function createHandlerForProvider(
|
||||
mode === "plan" ? options.planModeClinePassModelId : options.actModeClinePassModelId
|
||||
const configuredClinePassModelInfo =
|
||||
mode === "plan" ? options.planModeClinePassModelInfo : options.actModeClinePassModelInfo
|
||||
const clineModelId = configuredClinePassModelId?.startsWith("cline-pass/")
|
||||
? configuredClinePassModelId
|
||||
: clinePassDefaultModelId
|
||||
const clineModelInfo = configuredClinePassModelInfo || resolveClinePassModelInfo(clineModelId)
|
||||
// ClinePass users may also select Cline free models (OpenRouter-style ids without
|
||||
// the cline-pass/ prefix), so pass any configured id through to the Cline API.
|
||||
const clineModelId = configuredClinePassModelId || clinePassDefaultModelId
|
||||
const clineModelInfo = resolveClinePassModelInfo(
|
||||
clineModelId,
|
||||
configuredClinePassModelInfo
|
||||
? buildModelInfoNameMap({ [clineModelId]: configuredClinePassModelInfo })
|
||||
: undefined,
|
||||
)
|
||||
return new ClineHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
clineAccountId: options.clineAccountId,
|
||||
|
||||
@@ -220,6 +220,11 @@ describe("AwsBedrockHandler", () => {
|
||||
bedrockModels["anthropic.claude-fable-5:1m"].supportsGlobalEndpoint.should.equal(true)
|
||||
})
|
||||
|
||||
it("should mark Bedrock Sonnet 5 variants as global-endpoint capable", () => {
|
||||
bedrockModels["anthropic.claude-sonnet-5"].supportsGlobalEndpoint.should.equal(true)
|
||||
bedrockModels["anthropic.claude-sonnet-5:1m"].supportsGlobalEndpoint.should.equal(true)
|
||||
})
|
||||
|
||||
it("should include Vertex Opus 4.7 variants in the derived global model list", () => {
|
||||
vertexModels["claude-opus-4-7"].supportsGlobalEndpoint.should.equal(true)
|
||||
vertexModels["claude-opus-4-7:1m"].supportsGlobalEndpoint.should.equal(true)
|
||||
@@ -990,6 +995,19 @@ describe("AwsBedrockHandler", () => {
|
||||
modelId.should.equal("jp.anthropic.claude-sonnet-4-6")
|
||||
})
|
||||
|
||||
it("should apply JP cross-region prefix for sonnet 5", async () => {
|
||||
const jpOptions: AwsBedrockHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
apiModelId: "anthropic.claude-sonnet-5",
|
||||
awsRegion: "ap-northeast-1",
|
||||
}
|
||||
const jpHandler = new AwsBedrockHandler(jpOptions)
|
||||
|
||||
const modelId = await jpHandler.getModelId()
|
||||
modelId.should.equal("jp.anthropic.claude-sonnet-5")
|
||||
})
|
||||
|
||||
it("should apply global cross-region prefix for supported models", async () => {
|
||||
const globalOptions: AwsBedrockHandlerOptions = {
|
||||
...mockOptions,
|
||||
|
||||
@@ -426,6 +426,26 @@ describe("ClaudeCodeHandler", () => {
|
||||
model.info.contextWindow.should.equal(200_000)
|
||||
})
|
||||
|
||||
it("should support Sonnet 5 model id", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "claude-sonnet-5",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("claude-sonnet-5")
|
||||
model.info.contextWindow.should.equal(200_000)
|
||||
})
|
||||
|
||||
it("should support Sonnet 5 1m model id", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "claude-sonnet-5[1m]",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("claude-sonnet-5[1m]")
|
||||
model.info.contextWindow.should.equal(1_000_000)
|
||||
})
|
||||
|
||||
it("should support Fable 5 1m model id", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "claude-fable-5[1m]",
|
||||
|
||||
@@ -180,7 +180,7 @@ describe("ClineHandler", () => {
|
||||
const fakeClient = { chat: { completions: { create: sinon.stub().rejects(apiError) } } }
|
||||
sinon.stub(handler as any, "ensureClient").resolves(fakeClient as any)
|
||||
sinon.stub(handler as any, "getFreeModelIdSet").resolves(new Set())
|
||||
sinon.stub(handler, "getModel").returns({ id: "cline-pass/glm-5.1", info: openRouterDefaultModelInfo })
|
||||
sinon.stub(handler, "getModel").returns({ id: "cline-pass/glm-5.2", info: openRouterDefaultModelInfo })
|
||||
|
||||
let thrown: unknown
|
||||
try {
|
||||
@@ -191,7 +191,7 @@ describe("ClineHandler", () => {
|
||||
thrown = e
|
||||
}
|
||||
|
||||
const clineError = ClineError.transform(thrown, "cline-pass/glm-5.1", "cline-pass")
|
||||
const clineError = ClineError.transform(thrown, "cline-pass/glm-5.2", "cline-pass")
|
||||
clineError.isErrorType(ClineErrorType.Entitlement).should.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,6 +69,7 @@ describe("SapAiCoreHandler", () => {
|
||||
|
||||
it("should support different Claude model variants", () => {
|
||||
const modelVariants = [
|
||||
"anthropic--claude-sonnet-5",
|
||||
"anthropic--claude-4.6-sonnet",
|
||||
"anthropic--claude-4-sonnet",
|
||||
"anthropic--claude-4-opus",
|
||||
|
||||
@@ -134,9 +134,11 @@ interface ProviderChainOptions {
|
||||
profile?: string
|
||||
}
|
||||
|
||||
// a special jp inference profile was created for sonnet 4.6, opus 4.6, sonnet 4.5 & haiku 4.5
|
||||
// a special jp inference profile was created for newer Claude CRIS models
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
|
||||
const JP_SUPPORTED_CRIS_MODELS = [
|
||||
"anthropic.claude-sonnet-5",
|
||||
"anthropic.claude-sonnet-5:1m",
|
||||
"anthropic.claude-sonnet-4-6",
|
||||
"anthropic.claude-sonnet-4-6:1m",
|
||||
"anthropic.claude-opus-4-6-v1",
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
|
||||
@@ -672,6 +672,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
const anthropicModels = [
|
||||
"anthropic--claude-4.5-haiku",
|
||||
"anthropic--claude-sonnet-5",
|
||||
"anthropic--claude-4.7-opus",
|
||||
"anthropic--claude-4.6-opus",
|
||||
"anthropic--claude-4.5-opus",
|
||||
@@ -725,6 +726,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
"amazon--nova-micro",
|
||||
];
|
||||
const converseStreamModels = [
|
||||
"anthropic--claude-sonnet-5",
|
||||
"anthropic--claude-4.7-opus",
|
||||
"anthropic--claude-4.6-opus",
|
||||
"anthropic--claude-4.5-opus",
|
||||
|
||||
@@ -78,6 +78,19 @@ describe("createOpenRouterStream", () => {
|
||||
payload.should.not.have.property("max_tokens")
|
||||
})
|
||||
|
||||
it("strips the custom Sonnet 5 1m suffix and pins Anthropic/Vertex routing", async () => {
|
||||
const { client, create } = createClient()
|
||||
|
||||
await createOpenRouterStream(client as any, "system prompt", [{ role: "user", content: "hello" }] as any, {
|
||||
id: "anthropic/claude-sonnet-5:1m",
|
||||
info: createModelInfo(128_000),
|
||||
})
|
||||
|
||||
const payload = create.firstCall.args[0] as any
|
||||
payload.should.have.property("model", "anthropic/claude-sonnet-5")
|
||||
payload.provider.should.deepEqual({ order: ["anthropic", "google-vertex/global"], allow_fallbacks: false })
|
||||
})
|
||||
|
||||
it("adds cache_control blocks for Qwen models that require explicit OpenRouter caching", async () => {
|
||||
for (const modelId of ["qwen/qwen3.6-plus", "qwen/qwen3.7-max"]) {
|
||||
const { client, create } = createClient()
|
||||
@@ -114,4 +127,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.2",
|
||||
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" })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
openRouterClaudeSonnet461mModelId,
|
||||
openRouterClaudeSonnet51mModelId,
|
||||
} from "@shared/api"
|
||||
import { normalizeOpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { isClaudeOpusAdaptiveThinkingModel, resolveClaudeOpusAdaptiveThinking } from "@shared/utils/reasoning-support"
|
||||
@@ -62,6 +63,7 @@ export async function createOpenRouterStream(
|
||||
model.id === openRouterClaudeSonnet41mModelId ||
|
||||
model.id === openRouterClaudeSonnet451mModelId ||
|
||||
model.id === openRouterClaudeSonnet461mModelId ||
|
||||
model.id === openRouterClaudeSonnet51mModelId ||
|
||||
model.id === openRouterClaudeOpus461mModelId ||
|
||||
model.id === openRouterClaudeOpus471mModelId ||
|
||||
model.id === openRouterClaudeOpus481mModelId ||
|
||||
@@ -147,6 +149,8 @@ export async function createOpenRouterStream(
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-sonnet-5":
|
||||
case "anthropic/claude-5-sonnet":
|
||||
case "anthropic/claude-sonnet-4.6":
|
||||
case "anthropic/claude-4.6-sonnet":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
openRouterClaudeSonnet461mModelId,
|
||||
openRouterClaudeSonnet51mModelId,
|
||||
} from "@shared/api"
|
||||
import { normalizeOpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { isClaudeOpusAdaptiveThinkingModel, resolveClaudeOpusAdaptiveThinking } from "@shared/utils/reasoning-support"
|
||||
@@ -38,6 +39,7 @@ export async function createVercelAIGatewayStream(
|
||||
model.id === openRouterClaudeSonnet41mModelId ||
|
||||
model.id === openRouterClaudeSonnet451mModelId ||
|
||||
model.id === openRouterClaudeSonnet461mModelId ||
|
||||
model.id === openRouterClaudeSonnet51mModelId ||
|
||||
model.id === openRouterClaudeOpus461mModelId ||
|
||||
model.id === openRouterClaudeOpus471mModelId ||
|
||||
model.id === openRouterClaudeOpus481mModelId ||
|
||||
|
||||
@@ -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"
|
||||
@@ -523,8 +524,7 @@ export class Controller {
|
||||
|
||||
// On login we route the user to the managed "cline" provider, but preserve a
|
||||
// "cline-pass" selection made during onboarding (otherwise it would be clobbered).
|
||||
// A "cline-pass" provider can only be set when the ext-cline-pass flag is on, so
|
||||
// non-ClinePass logins are unaffected.
|
||||
// Non-ClinePass logins are unaffected because they do not persist "cline-pass".
|
||||
const planProvider: ApiProvider =
|
||||
currentApiConfiguration.planModeApiProvider === "cline-pass" ? "cline-pass" : "cline"
|
||||
const actProvider: ApiProvider = currentApiConfiguration.actModeApiProvider === "cline-pass" ? "cline-pass" : "cline"
|
||||
@@ -856,7 +856,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)
|
||||
})
|
||||
})
|
||||
|
||||
+98
-3
@@ -39,7 +39,7 @@ describe("refreshClineRecommendedModels", () => {
|
||||
data: {
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
description: "Remote recommended",
|
||||
tags: ["NEW"],
|
||||
},
|
||||
@@ -61,8 +61,8 @@ describe("refreshClineRecommendedModels", () => {
|
||||
expect(result).to.deep.equal({
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "anthropic/claude-sonnet-4.6",
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
name: "anthropic/claude-sonnet-5",
|
||||
description: "Remote recommended",
|
||||
tags: ["NEW"],
|
||||
},
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
openRouterClaudeSonnet461mModelId,
|
||||
openRouterClaudeSonnet51mModelId,
|
||||
} from "@/shared/api"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
@@ -87,6 +88,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 +139,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
|
||||
@@ -181,6 +213,15 @@ async function fetchAndCacheClineModels(): Promise<Record<string, ModelInfo>> {
|
||||
|
||||
// Apply model-specific overrides for known models
|
||||
switch (rawModel.id) {
|
||||
case "anthropic/claude-sonnet-5":
|
||||
case "anthropic/claude-5-sonnet":
|
||||
modelInfo.contextWindow = 200_000
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.inputPrice = 2.0
|
||||
modelInfo.outputPrice = 10.0
|
||||
modelInfo.cacheWritesPrice = 2.5
|
||||
modelInfo.cacheReadsPrice = 0.2
|
||||
break
|
||||
case "anthropic/claude-sonnet-4.6":
|
||||
case "anthropic/claude-4.6-sonnet":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
@@ -273,12 +314,17 @@ async function fetchAndCacheClineModels(): Promise<Record<string, ModelInfo>> {
|
||||
rawModel.id === "anthropic/claude-sonnet-4" ||
|
||||
rawModel.id === "anthropic/claude-sonnet-4.5" ||
|
||||
rawModel.id === "anthropic/claude-sonnet-4.6" ||
|
||||
rawModel.id === "anthropic/claude-4.6-sonnet"
|
||||
rawModel.id === "anthropic/claude-4.6-sonnet" ||
|
||||
rawModel.id === "anthropic/claude-sonnet-5" ||
|
||||
rawModel.id === "anthropic/claude-5-sonnet"
|
||||
) {
|
||||
const claudeSonnet1mModelInfo = cloneDeep(modelInfo)
|
||||
claudeSonnet1mModelInfo.contextWindow = 1_000_000
|
||||
claudeSonnet1mModelInfo.tiers = CLAUDE_SONNET_1M_TIERS
|
||||
|
||||
if (rawModel.id === "anthropic/claude-sonnet-5" || rawModel.id === "anthropic/claude-5-sonnet") {
|
||||
models[openRouterClaudeSonnet51mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4") {
|
||||
models[openRouterClaudeSonnet41mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
@@ -319,8 +365,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 +387,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
openRouterClaudeSonnet461mModelId,
|
||||
openRouterClaudeSonnet51mModelId,
|
||||
} from "@/shared/api"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -150,6 +151,16 @@ async function fetchAndCacheModels(controller: Controller): Promise<Record<strin
|
||||
}
|
||||
|
||||
switch (rawModel.id) {
|
||||
case "anthropic/claude-sonnet-5":
|
||||
case "anthropic/claude-5-sonnet":
|
||||
// NOTE: we artificially restrict the context window to 200k to keep costs low for users, and have a :1m model variant created below for users that want to use the full 1m.
|
||||
modelInfo.contextWindow = 200_000
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.inputPrice = 2.0
|
||||
modelInfo.outputPrice = 10.0
|
||||
modelInfo.cacheWritesPrice = 2.5
|
||||
modelInfo.cacheReadsPrice = 0.2
|
||||
break
|
||||
case "anthropic/claude-sonnet-4.6":
|
||||
case "anthropic/claude-4.6-sonnet":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
@@ -294,11 +305,17 @@ async function fetchAndCacheModels(controller: Controller): Promise<Record<strin
|
||||
rawModel.id === "anthropic/claude-sonnet-4.5" ||
|
||||
rawModel.id === "anthropic/claude-4.5-sonnet" ||
|
||||
rawModel.id === "anthropic/claude-sonnet-4.6" ||
|
||||
rawModel.id === "anthropic/claude-4.6-sonnet"
|
||||
rawModel.id === "anthropic/claude-4.6-sonnet" ||
|
||||
rawModel.id === "anthropic/claude-sonnet-5" ||
|
||||
rawModel.id === "anthropic/claude-5-sonnet"
|
||||
) {
|
||||
const claudeSonnet1mModelInfo = cloneDeep(modelInfo)
|
||||
claudeSonnet1mModelInfo.contextWindow = 1_000_000 // limiting providers to those that support 1m context window
|
||||
claudeSonnet1mModelInfo.tiers = CLAUDE_SONNET_1M_TIERS
|
||||
// sonnet 5
|
||||
if (rawModel.id === "anthropic/claude-sonnet-5" || rawModel.id === "anthropic/claude-5-sonnet") {
|
||||
models[openRouterClaudeSonnet51mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
// sonnet 4
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4") {
|
||||
models[openRouterClaudeSonnet41mModelId] = claudeSonnet1mModelInfo
|
||||
|
||||
@@ -19,19 +19,16 @@ describe("Hook System", () => {
|
||||
await writeHookScriptForPlatform(hookPath, nodeScript)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_TEST_TIMEOUT_MS)
|
||||
}
|
||||
setDistinctId("test-id")
|
||||
hookTestEnv = await createHookTestEnv()
|
||||
tempDir = hookTestEnv.tempDir
|
||||
sandbox = hookTestEnv.sandbox
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_TEST_TIMEOUT_MS)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await hookTestEnv.cleanup()
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PromptBuilder } from "../registry/PromptBuilder"
|
||||
import { SystemPromptSection } from "../templates/placeholders"
|
||||
import type { ComponentRegistry, PromptVariant, SystemPromptContext } from "../types"
|
||||
import { createVariant } from "../variants/variant-builder"
|
||||
import { mockProviderInfo } from "./integration.test"
|
||||
import { mockProviderInfo } from "./test-fixtures"
|
||||
|
||||
describe("PromptBuilder", () => {
|
||||
const mockContext: SystemPromptContext = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { PromptRegistry } from "../registry/PromptRegistry"
|
||||
import type { SystemPromptContext } from "../types"
|
||||
import { mockProviderInfo } from "./integration.test"
|
||||
import { mockProviderInfo } from "./test-fixtures"
|
||||
|
||||
describe("PromptRegistry", () => {
|
||||
let registry: PromptRegistry
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from "chai"
|
||||
import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { SystemPromptContext } from "../types"
|
||||
import { mockProviderInfo } from "./integration.test"
|
||||
import { mockProviderInfo } from "./test-fixtures"
|
||||
|
||||
describe("TemplateEngine", () => {
|
||||
let templateEngine: TemplateEngine
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { getSystemPrompt } from "../index"
|
||||
import type { SystemPromptContext } from "../types"
|
||||
import { mockProviderInfo } from "./test-fixtures"
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
@@ -111,12 +112,6 @@ async function assertSnapshot(name: string, content: string): Promise<void> {
|
||||
// Test Context Helpers
|
||||
// ============================================================================
|
||||
|
||||
export const mockProviderInfo = {
|
||||
providerId: "test",
|
||||
model: { id: "fast", info: { supportsPromptCache: false } },
|
||||
mode: "act" as const,
|
||||
}
|
||||
|
||||
const makeProviderInfo = (modelId: string, providerId = "test") => ({
|
||||
providerId: modelId.includes("ollama") ? "ollama" : providerId,
|
||||
model: { ...mockProviderInfo.model, id: modelId },
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ClineToolSet } from "../registry/ClineToolSet"
|
||||
import { PromptRegistry } from "../registry/PromptRegistry"
|
||||
import { new_task_variants } from "../tools/new_task"
|
||||
import type { SystemPromptContext } from "../types"
|
||||
import { mockProviderInfo } from "./integration.test"
|
||||
import { mockProviderInfo } from "./test-fixtures"
|
||||
|
||||
const baseContext: SystemPromptContext = {
|
||||
cwd: "/test/project",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Shared provider info fixture used across the system-prompt test suite.
|
||||
// Kept in a non-`.test.ts` file (like matcher-test.ts) so it can be exported
|
||||
// and imported by sibling test files without tripping biome's noExportsInTest rule.
|
||||
export const mockProviderInfo = {
|
||||
providerId: "test",
|
||||
model: { id: "fast", info: { supportsPromptCache: false } },
|
||||
mode: "act" as const,
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -2472,6 +2467,11 @@ export class Task {
|
||||
const isOrgClinePassRestrictionError = clineError.isErrorType(
|
||||
ClineErrorType.OrgClinePassRestriction,
|
||||
);
|
||||
// ClinePass period limits reset in hours/days — auto-retrying is
|
||||
// pointless and only delays the actionable error UI.
|
||||
const isClinePassLimitError = clineError.isErrorType(
|
||||
ClineErrorType.ClinePassLimit,
|
||||
);
|
||||
|
||||
// Check if this is a Cline provider insufficient credits error - don't auto-retry these
|
||||
const isClineProviderInsufficientCredits = (() => {
|
||||
@@ -2499,6 +2499,7 @@ export class Task {
|
||||
!quotaExceeded &&
|
||||
!isEntitlementError &&
|
||||
!isOrgClinePassRestrictionError &&
|
||||
!isClinePassLimitError &&
|
||||
this.taskState.autoRetryAttempts < 3;
|
||||
if (shouldRetry) {
|
||||
// Auto-retry enabled with max 3 attempts: automatically approve the retry
|
||||
@@ -2562,7 +2563,8 @@ export class Task {
|
||||
!isSpendLimitError &&
|
||||
!quotaExceeded &&
|
||||
!isEntitlementError &&
|
||||
!isOrgClinePassRestrictionError;
|
||||
!isOrgClinePassRestrictionError &&
|
||||
!isClinePassLimitError;
|
||||
if (showRetry) {
|
||||
await this.say(
|
||||
"error_retry",
|
||||
@@ -2849,7 +2851,7 @@ export class Task {
|
||||
"mistake_limit_reached",
|
||||
this.api.getModel().id.includes("claude")
|
||||
? `This may indicate a failure in Cline's thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. "Try breaking down the task into smaller steps").`
|
||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 4.5 Sonnet for its advanced agentic coding capabilities.",
|
||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use a stronger model with better tool-calling and agentic coding capabilities.",
|
||||
);
|
||||
if (response === "messageResponse") {
|
||||
// Display the user's message in the chat UI
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum ClineErrorType {
|
||||
QuotaExceeded = "quotaExceeded",
|
||||
Entitlement = "entitlement",
|
||||
OrgClinePassRestriction = "orgClinePassRestriction",
|
||||
ClinePassLimit = "clinePassLimit",
|
||||
}
|
||||
|
||||
interface ErrorDetails {
|
||||
@@ -57,6 +58,46 @@ const ORG_CLINE_PASS_RESTRICTION_MESSAGE =
|
||||
const ORG_CLINE_PASS_RESTRICTION_USER_MESSAGE =
|
||||
"organization accounts cannot use clinepass subscriptions";
|
||||
|
||||
// The ClinePass period limit message is dynamic ("weekly"/"5-hour" period, "7d"/"12h"
|
||||
// reset), so it is matched by its fixed prefix/suffix with the ClinePass marker in
|
||||
// between. Plain indexOf scanning — no regex, so no backtracking on hostile input.
|
||||
const CLINE_PASS_LIMIT_PREFIX = "you have reached your";
|
||||
const CLINE_PASS_LIMIT_MARKER = "clinepass limit";
|
||||
const CLINE_PASS_LIMIT_SUFFIX = "please try again later.";
|
||||
|
||||
function findClinePassLimitMessageBounds(
|
||||
text: string,
|
||||
): { start: number; end: number } | undefined {
|
||||
const normalized = text.toLowerCase();
|
||||
const start = normalized.indexOf(CLINE_PASS_LIMIT_PREFIX);
|
||||
if (start === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const suffixStart = normalized.indexOf(CLINE_PASS_LIMIT_SUFFIX, start);
|
||||
if (suffixStart === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const end = suffixStart + CLINE_PASS_LIMIT_SUFFIX.length;
|
||||
if (!normalized.slice(start, end).includes(CLINE_PASS_LIMIT_MARKER)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
export function isClinePassLimitMessage(text: string): boolean {
|
||||
return findClinePassLimitMessageBounds(text) !== undefined;
|
||||
}
|
||||
|
||||
export function extractClinePassLimitMessage(
|
||||
text: string,
|
||||
): string | undefined {
|
||||
const bounds = findClinePassLimitMessageBounds(text);
|
||||
return bounds ? text.slice(bounds.start, bounds.end) : undefined;
|
||||
}
|
||||
|
||||
export class ClineError extends Error {
|
||||
readonly title = "ClineError";
|
||||
readonly _error: ErrorDetails;
|
||||
@@ -202,6 +243,18 @@ export class ClineError extends Error {
|
||||
return ClineErrorType.Entitlement;
|
||||
}
|
||||
|
||||
// ClinePass period limits (weekly/5-hour) are user-actionable (switch to
|
||||
// usage-based billing) and must not fall through to the generic 403 auth
|
||||
// handling below or the 429 rate-limit patterns.
|
||||
const detailMessage =
|
||||
typeof details?.message === "string" ? details.message : undefined;
|
||||
if (
|
||||
isClinePassLimitMessage(detailMessage ?? "") ||
|
||||
isClinePassLimitMessage(message ?? "")
|
||||
) {
|
||||
return ClineErrorType.ClinePassLimit;
|
||||
}
|
||||
|
||||
// Check auth errors
|
||||
const isAuthStatus = status !== undefined && status > 400 && status < 429;
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, it } from "mocha";
|
||||
import "should";
|
||||
import { ClineError, ClineErrorType } from "../ClineError";
|
||||
import {
|
||||
ClineError,
|
||||
ClineErrorType,
|
||||
extractClinePassLimitMessage,
|
||||
isClinePassLimitMessage,
|
||||
} from "../ClineError";
|
||||
|
||||
describe("ClineError", () => {
|
||||
describe("getErrorType", () => {
|
||||
@@ -59,7 +64,7 @@ describe("ClineError", () => {
|
||||
"Error 403: the user is not subscribed to required model plan",
|
||||
},
|
||||
},
|
||||
"cline-pass/glm-5.1",
|
||||
"cline-pass/glm-5.2",
|
||||
"cline-pass",
|
||||
);
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.Entitlement);
|
||||
@@ -89,5 +94,74 @@ describe("ClineError", () => {
|
||||
const result = ClineError.getErrorType(err);
|
||||
(result !== ClineErrorType.OrgClinePassRestriction).should.be.true();
|
||||
});
|
||||
|
||||
it("should classify ClinePass period limit messages as ClinePassLimit", () => {
|
||||
const err = new ClineError(
|
||||
"You have reached your weekly Clinepass limit. The limit resets in 7d, please try again later.",
|
||||
);
|
||||
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.ClinePassLimit);
|
||||
});
|
||||
|
||||
it("should classify nested ClinePass period limit messages as ClinePassLimit", () => {
|
||||
// ClineError maps `error.error` into `details`, matching the real provider error shape.
|
||||
const err = new ClineError({
|
||||
message: "403 Error 403",
|
||||
error: {
|
||||
message:
|
||||
"You have reached your monthly ClinePass limit. The limit resets in 12h, please try again later.",
|
||||
},
|
||||
});
|
||||
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.ClinePassLimit);
|
||||
});
|
||||
|
||||
it("should prefer ClinePassLimit over Auth for a 403 with the limit message", () => {
|
||||
// status 403 falls inside the generic auth-status range; the limit message must win.
|
||||
const err = new ClineError({
|
||||
message:
|
||||
"You have reached your 5-hour Clinepass limit. The limit resets in 5h, please try again later.",
|
||||
status: 403,
|
||||
});
|
||||
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.ClinePassLimit);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isClinePassLimitMessage", () => {
|
||||
it("matches limit messages with variable period and reset", () => {
|
||||
isClinePassLimitMessage(
|
||||
"You have reached your weekly Clinepass limit. The limit resets in 7d, please try again later.",
|
||||
).should.be.true();
|
||||
isClinePassLimitMessage(
|
||||
"You have reached your 5-hour ClinePass limit. The limit resets in 5h, please try again later.",
|
||||
).should.be.true();
|
||||
});
|
||||
|
||||
it("does not match unrelated or partial messages", () => {
|
||||
isClinePassLimitMessage(
|
||||
"the user is not subscribed to required model plan",
|
||||
).should.be.false();
|
||||
isClinePassLimitMessage(
|
||||
`You have reached your\t-\tClinepass limit.The limit resets in\t${"\t".repeat(10_000)}`,
|
||||
).should.be.false();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractClinePassLimitMessage", () => {
|
||||
it("extracts the limit message out of a wrapped error string", () => {
|
||||
const message =
|
||||
"You have reached your weekly Clinepass limit. The limit resets in 7d, please try again later.";
|
||||
|
||||
extractClinePassLimitMessage(`429 Error: ${message}`)!.should.equal(
|
||||
message,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined when there is no limit message", () => {
|
||||
(
|
||||
extractClinePassLimitMessage("some other error") === undefined
|
||||
).should.be.true();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { expect } from "chai"
|
||||
import {
|
||||
buildModelInfoNameMap,
|
||||
clinePassModelInfoSaneDefaults,
|
||||
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 })
|
||||
})
|
||||
|
||||
it("returns stored info for a free (non cline-pass prefixed) model id", () => {
|
||||
const freeModelInfo = createModelInfo("Trinity Large Preview", 512_000)
|
||||
const modelInfo = resolveClinePassModelInfo(
|
||||
"arcee-ai/trinity-large-preview:free",
|
||||
buildModelInfoNameMap({ "arcee-ai/trinity-large-preview:free": freeModelInfo }),
|
||||
)
|
||||
|
||||
expect(modelInfo).to.deep.equal(freeModelInfo)
|
||||
})
|
||||
|
||||
it("falls back to sane defaults for a free model id without dynamic metadata", () => {
|
||||
const modelInfo = resolveClinePassModelInfo("kwaipilot/kat-coder-pro")
|
||||
|
||||
expect(modelInfo).to.deep.equal(clinePassModelInfoSaneDefaults)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
+141
-31
@@ -179,10 +179,33 @@ export const hicapModelInfoSaneDefaults: HicapCompatibleModelInfo = {
|
||||
// Anthropic
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
|
||||
export type AnthropicModelId = keyof typeof anthropicModels
|
||||
export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-5-20250929"
|
||||
export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-5"
|
||||
export const ANTHROPIC_MIN_THINKING_BUDGET = 1_024
|
||||
export const ANTHROPIC_MAX_THINKING_BUDGET = 6_000
|
||||
export const anthropicModels = {
|
||||
"claude-sonnet-5": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"claude-sonnet-5:1m": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
maxTokens: 64_000,
|
||||
contextWindow: 200_000,
|
||||
@@ -471,15 +494,15 @@ export const anthropicModels = {
|
||||
|
||||
// Claude Code
|
||||
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
|
||||
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-5-20250929"
|
||||
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-5"
|
||||
export const claudeCodeModels = {
|
||||
sonnet: {
|
||||
...anthropicModels["claude-sonnet-4-5-20250929"],
|
||||
...anthropicModels["claude-sonnet-5"],
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
"sonnet[1m]": {
|
||||
...anthropicModels["claude-sonnet-4-5-20250929:1m"],
|
||||
...anthropicModels["claude-sonnet-5:1m"],
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
@@ -499,6 +522,16 @@ export const claudeCodeModels = {
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
"claude-sonnet-5": {
|
||||
...anthropicModels["claude-sonnet-5"],
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
"claude-sonnet-5[1m]": {
|
||||
...anthropicModels["claude-sonnet-5:1m"],
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
...anthropicModels["claude-sonnet-4-6"],
|
||||
supportsImages: false,
|
||||
@@ -597,8 +630,33 @@ export const claudeCodeModels = {
|
||||
// AWS Bedrock
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
|
||||
export type BedrockModelId = keyof typeof bedrockModels
|
||||
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-sonnet-5"
|
||||
export const bedrockModels = {
|
||||
"anthropic.claude-sonnet-5": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"anthropic.claude-sonnet-5:1m": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
},
|
||||
"anthropic.claude-sonnet-4-6": {
|
||||
maxTokens: 64_000,
|
||||
contextWindow: 200_000,
|
||||
@@ -991,25 +1049,26 @@ export const bedrockModels = {
|
||||
|
||||
// OpenRouter
|
||||
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
||||
export const openRouterDefaultModelId = "anthropic/claude-sonnet-4.5" // will always exist in openRouterModels
|
||||
export const openRouterDefaultModelId = "anthropic/claude-sonnet-5" // will always exist in openRouterModels
|
||||
export const openRouterClaudeSonnet41mModelId = `anthropic/claude-sonnet-4${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeSonnet451mModelId = `anthropic/claude-sonnet-4.5${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeSonnet461mModelId = `anthropic/claude-sonnet-4.6${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeSonnet51mModelId = `anthropic/claude-sonnet-5${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeOpus461mModelId = `anthropic/claude-opus-4.6${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeOpus471mModelId = `anthropic/claude-opus-4.7${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeOpus481mModelId = `anthropic/claude-opus-4.8${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeFable51mModelId = `anthropic/claude-fable-5${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 64_000,
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
description:
|
||||
"Claude Sonnet 4.5 delivers superior intelligence across coding, agentic search, and AI agent capabilities. It's a powerful choice for agentic coding, and can complete tasks across the entire software development lifecycle, from initial planning to bug fixes, maintenance to large refactors. It offers strong performance in both planning and solving for complex coding tasks, making it an ideal choice to power end-to-end software development processes.\n\nRead more in the [blog post here](https://www.anthropic.com/claude/sonnet)",
|
||||
"Claude Sonnet 5 is Anthropic's latest Sonnet model for coding, agents, and professional work. It supports adaptive thinking, prompt caching, image inputs, and long-context workflows.",
|
||||
}
|
||||
|
||||
// Cline custom model - Devstral
|
||||
@@ -1025,7 +1084,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,8 +1098,8 @@ export const clinePassModelInfoSaneDefaults: ModelInfo = {
|
||||
description: "",
|
||||
}
|
||||
export const clinePassModels = {
|
||||
"cline-pass/glm-5.1": {
|
||||
name: "cline-pass/glm-5.1",
|
||||
"cline-pass/glm-5.2": {
|
||||
name: "cline-pass/glm-5.2",
|
||||
maxTokens: 131_072,
|
||||
contextWindow: 202_752,
|
||||
supportsImages: false,
|
||||
@@ -1069,9 +1128,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
|
||||
)
|
||||
}
|
||||
@@ -1230,6 +1292,29 @@ export const vertexModels = {
|
||||
supportsThinkingLevel: true,
|
||||
},
|
||||
},
|
||||
"claude-sonnet-5": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
"claude-sonnet-5:1m": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
supportsReasoning: true,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
maxTokens: 64_000,
|
||||
contextWindow: 200_000,
|
||||
@@ -4360,18 +4445,18 @@ export const groqModels = {
|
||||
|
||||
// Requesty
|
||||
// https://requesty.ai/models
|
||||
export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest"
|
||||
export const requestyDefaultModelId = "anthropic/claude-sonnet-5"
|
||||
export const requestyDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
description: "Anthropic's most intelligent model. Highest level of intelligence and capability.",
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
cacheWritesPrice: 2.5,
|
||||
cacheReadsPrice: 0.2,
|
||||
description: "Anthropic's latest Sonnet model for coding, agents, and professional work.",
|
||||
}
|
||||
|
||||
// SAP AI Core
|
||||
@@ -4380,6 +4465,13 @@ export const sapAiCoreDefaultModelId: SapAiCoreModelId = "anthropic--claude-3.5-
|
||||
// Pricing is calculated using Capacity Units, not directly in USD
|
||||
const sapAiCoreModelDescription = "Pricing is calculated using SAP's Capacity Units rather than direct USD pricing."
|
||||
export const sapAiCoreModels = {
|
||||
"anthropic--claude-sonnet-5": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
description: sapAiCoreModelDescription,
|
||||
},
|
||||
"anthropic--claude-4.5-haiku": {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 200_000,
|
||||
@@ -4983,12 +5075,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 +5156,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,
|
||||
@@ -5193,8 +5304,7 @@ export const fireworksModels = {
|
||||
outputPrice: 8,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0.3,
|
||||
description:
|
||||
"Kimi K2.6 Turbo router for high-performance agentic workloads with vision and text reasoning.",
|
||||
description: "Kimi K2.6 Turbo router for high-performance agentic workloads with vision and text reasoning.",
|
||||
},
|
||||
"accounts/fireworks/routers/kimi-k2p6-fast": {
|
||||
maxTokens: 262000,
|
||||
@@ -5205,8 +5315,7 @@ export const fireworksModels = {
|
||||
outputPrice: 8,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0.3,
|
||||
description:
|
||||
"Kimi K2.6 Fast router for high-performance agentic workloads with vision and text reasoning.",
|
||||
description: "Kimi K2.6 Fast router for high-performance agentic workloads with vision and text reasoning.",
|
||||
},
|
||||
"accounts/fireworks/routers/kimi-k2p7-code-fast": {
|
||||
maxTokens: 262000,
|
||||
@@ -5217,8 +5326,7 @@ export const fireworksModels = {
|
||||
outputPrice: 8,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0.38,
|
||||
description:
|
||||
"Kimi K2.7 Code Fast router for high-performance coding workloads with vision and text reasoning.",
|
||||
description: "Kimi K2.7 Code Fast router for high-performance coding workloads with vision and text reasoning.",
|
||||
},
|
||||
"accounts/fireworks/models/deepseek-v4-flash": {
|
||||
maxTokens: 384000,
|
||||
@@ -5253,7 +5361,8 @@ export const fireworksModels = {
|
||||
outputPrice: 4.4,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0.26,
|
||||
description: "GLM 5.2 is a next-generation general-purpose model optimized for coding, reasoning, and agentic workflows with a 1M context window.",
|
||||
description:
|
||||
"GLM 5.2 is a next-generation general-purpose model optimized for coding, reasoning, and agentic workflows with a 1M context window.",
|
||||
},
|
||||
"accounts/fireworks/models/glm-5p1": {
|
||||
maxTokens: 131072,
|
||||
@@ -5297,7 +5406,8 @@ export const fireworksModels = {
|
||||
outputPrice: 1.2,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0.06,
|
||||
description: "MiniMax M2.7 is tuned for strong real-world performance across coding, agent-driven, and workflow-heavy tasks.",
|
||||
description:
|
||||
"MiniMax M2.7 is tuned for strong real-world performance across coding, agent-driven, and workflow-heavy tasks.",
|
||||
},
|
||||
"accounts/fireworks/models/qwen3p7-plus": {
|
||||
maxTokens: 262144,
|
||||
|
||||
@@ -55,8 +55,8 @@ export const CLINE_ONBOARDING_MODELS: OnboardingModel[] = [
|
||||
},
|
||||
{
|
||||
group: "frontier",
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
name: "Anthropic: Claude Sonnet 4.5",
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
name: "Anthropic: Claude Sonnet 5",
|
||||
badge: "Best",
|
||||
score: 97,
|
||||
latency: 3,
|
||||
@@ -64,8 +64,8 @@ export const CLINE_ONBOARDING_MODELS: OnboardingModel[] = [
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
tiers: [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const CLINE_RECOMMENDED_MODELS_FALLBACK: ClineRecommendedModelsData = {
|
||||
tags: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "Anthropic Claude Sonnet 4.6",
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
name: "Anthropic Claude Sonnet 5",
|
||||
description: "Latest Sonnet release with strong coding and agent performance",
|
||||
tags: ["NEW"],
|
||||
},
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { normalizeOpenaiReasoningEffort, type OpenaiReasoningEffort } from "../storage/types"
|
||||
|
||||
export interface ClaudeOpusAdaptiveThinkingSettings {
|
||||
export interface ClaudeAdaptiveThinkingSettings {
|
||||
enabled: boolean
|
||||
effort?: OpenaiReasoningEffort
|
||||
}
|
||||
|
||||
export function isClaudeOpusAdaptiveThinkingModel(modelId?: string): boolean {
|
||||
export type ClaudeOpusAdaptiveThinkingSettings = ClaudeAdaptiveThinkingSettings
|
||||
|
||||
export function isClaudeAdaptiveThinkingModel(modelId?: string): boolean {
|
||||
if (!modelId) {
|
||||
return false
|
||||
}
|
||||
@@ -14,14 +16,18 @@ export function isClaudeOpusAdaptiveThinkingModel(modelId?: string): boolean {
|
||||
const adaptiveVersions = ["4-6", "4.6", "4-7", "4.7", "4-8", "4.8"]
|
||||
return (
|
||||
id.includes("claude-fable-5") ||
|
||||
id.includes("claude-sonnet-5") ||
|
||||
id.includes("claude-5-sonnet") ||
|
||||
adaptiveVersions.some((version) => id.includes(`claude-opus-${version}`) || id.includes(`claude-${version}-opus`))
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveClaudeOpusAdaptiveThinking(
|
||||
export const isClaudeOpusAdaptiveThinkingModel = isClaudeAdaptiveThinkingModel
|
||||
|
||||
export function resolveClaudeAdaptiveThinking(
|
||||
reasoningEffort?: string,
|
||||
legacyThinkingBudgetTokens?: number,
|
||||
): ClaudeOpusAdaptiveThinkingSettings {
|
||||
): ClaudeAdaptiveThinkingSettings {
|
||||
if (reasoningEffort) {
|
||||
const effort = normalizeOpenaiReasoningEffort(reasoningEffort)
|
||||
return effort === "none" ? { enabled: false } : { enabled: true, effort }
|
||||
@@ -30,6 +36,8 @@ export function resolveClaudeOpusAdaptiveThinking(
|
||||
return legacyThinkingBudgetTokens && legacyThinkingBudgetTokens > 0 ? { enabled: true, effort: "high" } : { enabled: false }
|
||||
}
|
||||
|
||||
export const resolveClaudeOpusAdaptiveThinking = resolveClaudeAdaptiveThinking
|
||||
|
||||
export function supportsReasoningEffortForModel(modelId?: string): boolean {
|
||||
if (!modelId) {
|
||||
return false
|
||||
@@ -37,11 +45,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")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,8 +94,8 @@ export const E2E_MOCK_CLINE_RECOMMENDED_MODELS = {
|
||||
],
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "anthropic/claude-sonnet-4.6",
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
name: "anthropic/claude-sonnet-5",
|
||||
description: "Recommended model for e2e onboarding",
|
||||
tags: ["BEST"],
|
||||
},
|
||||
@@ -123,12 +123,12 @@ export const E2E_MOCK_CLINE_MODELS = [
|
||||
supported_parameters: [],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "anthropic/claude-sonnet-4.6",
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
name: "anthropic/claude-sonnet-5",
|
||||
description: "Recommended model for e2e onboarding",
|
||||
context_length: 200_000,
|
||||
top_provider: {
|
||||
max_completion_tokens: 64_000,
|
||||
max_completion_tokens: 128_000,
|
||||
context_length: 200_000,
|
||||
is_moderated: false,
|
||||
},
|
||||
@@ -136,10 +136,10 @@ export const E2E_MOCK_CLINE_MODELS = [
|
||||
modality: "text->text",
|
||||
},
|
||||
pricing: {
|
||||
prompt: "0.000003",
|
||||
completion: "0.000015",
|
||||
input_cache_read: "0.0000003",
|
||||
input_cache_write: "0.00000375",
|
||||
prompt: "0.000002",
|
||||
completion: "0.00001",
|
||||
input_cache_read: "0.0000002",
|
||||
input_cache_write: "0.0000025",
|
||||
},
|
||||
supported_parameters: ["include_reasoning"],
|
||||
},
|
||||
|
||||
@@ -20,7 +20,9 @@ describe("Cline Extension", () => {
|
||||
})
|
||||
|
||||
it("should successfully execute the plus button command", async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 400))
|
||||
const packageJSON = JSON.parse(await readFile(packagePath, "utf8"))
|
||||
const id = packageJSON.publisher + "." + packageJSON.name
|
||||
await vscode.extensions.getExtension(id)?.activate()
|
||||
await vscode.commands.executeCommand("cline.plusButtonClicked")
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it } from "mocha"
|
||||
import "should"
|
||||
import type { ApiHandlerModel, ApiProviderInfo } from "@core/api"
|
||||
import { isClaudeAdaptiveThinkingModel } from "@shared/utils/reasoning-support"
|
||||
import {
|
||||
GEMINI_FLASH_MAX_OUTPUT_TOKENS,
|
||||
isClaude4PlusModelFamily,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
isPoolsideModelFamily,
|
||||
modelDoesntSupportWebp,
|
||||
shouldSkipReasoningForModel,
|
||||
supportsReasoningEffortForModel,
|
||||
} from "../model-utils"
|
||||
|
||||
// Minimal helper — modelDoesntSupportWebp only reads apiHandlerModel.id
|
||||
@@ -37,6 +39,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 +53,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)
|
||||
@@ -77,6 +105,18 @@ describe("isClaude4PlusModelFamily", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("isClaudeAdaptiveThinkingModel", () => {
|
||||
it("should return true for Claude Sonnet 5 IDs across provider naming variants", () => {
|
||||
for (const modelId of [
|
||||
"claude-sonnet-5",
|
||||
"anthropic/claude-sonnet-5:1m",
|
||||
"anthropic/claude-5-sonnet",
|
||||
]) {
|
||||
isClaudeAdaptiveThinkingModel(modelId).should.equal(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("isGPT5ModelFamily", () => {
|
||||
it("should return true for GPT-5 model IDs with hyphen", () => {
|
||||
isGPT5ModelFamily("gpt-5").should.equal(true)
|
||||
|
||||
@@ -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,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
switch (selectedProvider) {
|
||||
case "cline":
|
||||
return `${selectedProvider}:${selectedModelId}`
|
||||
case "cline-pass":
|
||||
// Free models selected on ClinePass go through Cline usage billing,
|
||||
// so label them the same way as the cline provider
|
||||
return selectedModelId.startsWith("cline-pass/")
|
||||
? `${selectedProvider}:${selectedModelId.replace(/^cline-pass\//, "")}`
|
||||
: `cline:${selectedModelId}`
|
||||
case "openai":
|
||||
return `openai-compat:${selectedModelId}`
|
||||
case "vscode-lm":
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react";
|
||||
import { useState } from "react";
|
||||
import { useApiConfigurationHandlers } from "@/components/settings/utils/useApiConfigurationHandlers";
|
||||
|
||||
interface ClinePassLimitErrorProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
const ClinePassLimitError = ({ message }: ClinePassLimitErrorProps) => {
|
||||
const { handleFieldsChange } = useApiConfigurationHandlers();
|
||||
const [isSwitching, setIsSwitching] = useState(false);
|
||||
const [didSwitch, setDidSwitch] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
|
||||
const handleSwitchToUsageBasedBilling = async () => {
|
||||
setIsSwitching(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
await handleFieldsChange({
|
||||
planModeApiProvider: "cline",
|
||||
actModeApiProvider: "cline",
|
||||
});
|
||||
setDidSwitch(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to switch to Cline usage-based billing:", error);
|
||||
setError(
|
||||
"Failed to switch provider. Select Cline Usage-Billing in API Configuration settings.",
|
||||
);
|
||||
} finally {
|
||||
setIsSwitching(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="p-2 border-none rounded-md mb-2 bg-(--vscode-textBlockQuote-background)"
|
||||
data-testid="cline-pass-limit-error"
|
||||
>
|
||||
<div className="text-error mb-2">ClinePass limit reached</div>
|
||||
<div className="text-(--vscode-descriptionForeground) text-xs wrap-anywhere">
|
||||
{message}
|
||||
</div>
|
||||
<div className="text-(--vscode-descriptionForeground) text-xs mt-2">
|
||||
Would you like to switch to Usage-Based billing and retry with the
|
||||
Cline provider?
|
||||
</div>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
className="w-full mt-3"
|
||||
disabled={isSwitching || didSwitch}
|
||||
onClick={handleSwitchToUsageBasedBilling}
|
||||
>
|
||||
{isSwitching
|
||||
? "Switching..."
|
||||
: didSwitch
|
||||
? "Switched to Usage-Based billing"
|
||||
: "Switch to Usage-Based billing"}
|
||||
</VSCodeButton>
|
||||
{didSwitch && (
|
||||
<div className="text-(--vscode-descriptionForeground) text-xs mt-2">
|
||||
Retry the request after switching.
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="text-error text-xs mt-2">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClinePassLimitError;
|
||||
@@ -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
|
||||
|
||||
@@ -223,7 +223,7 @@ export const ClinePassEntitlementError: Story = {
|
||||
message: "403 Error 403: the user is not subscribed to required model plan",
|
||||
status: 403,
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
modelId: "cline-pass/glm-5.1",
|
||||
modelId: "cline-pass/glm-5.2",
|
||||
providerId: "cline-pass",
|
||||
details: {
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage";
|
||||
import { ApiProvider } from "@shared/proto/cline/models";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import ErrorRow from "./ErrorRow";
|
||||
|
||||
const mockSetUserOrganization = vi.hoisted(() => vi.fn());
|
||||
const mockUpdateApiConfigurationProto = vi.hoisted(() => vi.fn());
|
||||
const mockApiConfiguration = vi.hoisted(() => ({
|
||||
planModeApiProvider: "cline-pass",
|
||||
actModeApiProvider: "cline-pass",
|
||||
planModeClinePassModelId: "cline-pass/test-plan-model",
|
||||
actModeClinePassModelId: "cline-pass/test-act-model",
|
||||
}));
|
||||
|
||||
// Mock the auth context
|
||||
vi.mock("@/context/ClineAuthContext", () => ({
|
||||
@@ -30,10 +38,19 @@ vi.mock("@/components/chat/EntitlementError", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
useExtensionState: () => ({
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/services/grpc-client", () => ({
|
||||
AccountServiceClient: {
|
||||
setUserOrganization: mockSetUserOrganization,
|
||||
},
|
||||
ModelsServiceClient: {
|
||||
updateApiConfigurationProto: mockUpdateApiConfigurationProto,
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock ClineError
|
||||
@@ -47,7 +64,9 @@ vi.mock("../../../../src/services/error/ClineError", () => ({
|
||||
Auth: "auth",
|
||||
Entitlement: "entitlement",
|
||||
OrgClinePassRestriction: "orgClinePassRestriction",
|
||||
ClinePassLimit: "clinePassLimit",
|
||||
},
|
||||
extractClinePassLimitMessage: vi.fn((text: string) => text),
|
||||
}));
|
||||
|
||||
describe("ErrorRow", () => {
|
||||
@@ -61,6 +80,7 @@ describe("ErrorRow", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSetUserOrganization.mockResolvedValue({});
|
||||
mockUpdateApiConfigurationProto.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("renders basic error message", () => {
|
||||
@@ -266,6 +286,83 @@ describe("ErrorRow", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders ClinePass limit error and switches to Cline usage-based billing", async () => {
|
||||
const limitMessage =
|
||||
"You have reached your weekly Clinepass limit. The limit resets in 7d, please try again later.";
|
||||
const mockClineError = {
|
||||
message: limitMessage,
|
||||
isErrorType: vi.fn((type) => type === "clinePassLimit"),
|
||||
providerId: "cline-pass",
|
||||
_error: {
|
||||
message: limitMessage,
|
||||
},
|
||||
};
|
||||
|
||||
const { ClineError } = await import(
|
||||
"../../../../src/services/error/ClineError"
|
||||
);
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any);
|
||||
|
||||
render(
|
||||
<ErrorRow
|
||||
apiRequestFailedMessage={limitMessage}
|
||||
errorType="error"
|
||||
message={mockMessage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("cline-pass-limit-error")).toBeInTheDocument();
|
||||
expect(screen.getByText(limitMessage)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("Switch to Usage-Based billing"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockUpdateApiConfigurationProto).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
// The proto conversion maps provider id strings to ApiProvider enum values.
|
||||
const request = mockUpdateApiConfigurationProto.mock.calls[0][0];
|
||||
expect(request.apiConfiguration.planModeApiProvider).toBe(
|
||||
ApiProvider.CLINE,
|
||||
);
|
||||
expect(request.apiConfiguration.actModeApiProvider).toBe(
|
||||
ApiProvider.CLINE,
|
||||
);
|
||||
expect(
|
||||
screen.getByText("Switched to Usage-Based billing"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not offer the usage-based billing switch when already on the cline provider", async () => {
|
||||
const limitMessage =
|
||||
"You have reached your weekly Clinepass limit. The limit resets in 7d, please try again later.";
|
||||
const mockClineError = {
|
||||
message: limitMessage,
|
||||
isErrorType: vi.fn((type) => type === "clinePassLimit"),
|
||||
providerId: "cline",
|
||||
_error: {
|
||||
message: limitMessage,
|
||||
},
|
||||
};
|
||||
|
||||
const { ClineError } = await import(
|
||||
"../../../../src/services/error/ClineError"
|
||||
);
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any);
|
||||
|
||||
render(
|
||||
<ErrorRow
|
||||
apiRequestFailedMessage={limitMessage}
|
||||
errorType="error"
|
||||
message={mockMessage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("cline-pass-limit-error"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByText(limitMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders friendly logged-out message and sign in button when user is not signed in", async () => {
|
||||
const mockClineError = {
|
||||
message: "Authentication failed",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage";
|
||||
import { isClineProvider } from "@shared/utils/cline";
|
||||
import { memo } from "react";
|
||||
import ClinePassLimitError from "@/components/chat/ClinePassLimitError";
|
||||
import CreditLimitError from "@/components/chat/CreditLimitError";
|
||||
import EntitlementError from "@/components/chat/EntitlementError";
|
||||
import OrgClinePassRestrictionError from "@/components/chat/OrgClinePassRestrictionError";
|
||||
@@ -10,6 +11,7 @@ import { useClineAuth, useClineSignIn } from "@/context/ClineAuthContext";
|
||||
import {
|
||||
ClineError,
|
||||
ClineErrorType,
|
||||
extractClinePassLimitMessage,
|
||||
} from "../../../../src/services/error/ClineError";
|
||||
|
||||
const _errorColor = "var(--vscode-errorForeground)";
|
||||
@@ -90,6 +92,19 @@ const ErrorRow = memo(
|
||||
return <OrgClinePassRestrictionError />;
|
||||
}
|
||||
|
||||
// Gated on the ClinePass provider: users already on usage-based
|
||||
// billing shouldn't be offered a switch to what they're on.
|
||||
if (
|
||||
clineError?.isErrorType(ClineErrorType.ClinePassLimit) &&
|
||||
providerId === "cline-pass"
|
||||
) {
|
||||
const detailMessage =
|
||||
clineError?._error?.details?.message || errorMessage;
|
||||
const limitMessage =
|
||||
extractClinePassLimitMessage(detailMessage) ?? detailMessage;
|
||||
return <ClinePassLimitError message={limitMessage} />;
|
||||
}
|
||||
|
||||
if (clineError?.isErrorType(ClineErrorType.RateLimit)) {
|
||||
return (
|
||||
<p className="m-0 whitespace-pre-wrap text-error wrap-anywhere">
|
||||
|
||||
+74
-6
@@ -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(() => {
|
||||
@@ -160,7 +165,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
break
|
||||
|
||||
case BannerActionType.SetModel: {
|
||||
const modelId = action.arg || "anthropic/claude-sonnet-4.5"
|
||||
const modelId = action.arg || "anthropic/claude-sonnet-5"
|
||||
const initialModelTab = action.tab || "recommended"
|
||||
handleFieldsChange({
|
||||
planModeOpenRouterModelId: modelId,
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { buildModelInfoNameMap, type ModelInfo, resolveClinePassModelInfo } from "@shared/api"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import type { OnboardingModel, OnboardingModelGroup, OpenRouterModelInfo } from "@shared/proto/index.cline"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { AlertCircleIcon, CircleCheckIcon, CircleIcon, ListIcon, LoaderCircleIcon, ZapIcon } from "lucide-react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import ClineLogoWhite from "@/assets/ClineLogoWhite"
|
||||
@@ -7,15 +9,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 { AccountServiceClient, StateServiceClient, UiServiceClient } from "@/services/grpc-client"
|
||||
import ApiConfigurationSection from "../settings/sections/ApiConfigurationSection"
|
||||
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
|
||||
import WelcomeView from "../welcome/WelcomeView"
|
||||
import { setPendingClinePassSubscribe } from "./clinePassSubscribe"
|
||||
import {
|
||||
getCapabilities,
|
||||
getClineUIOnboardingGroups,
|
||||
@@ -235,7 +235,25 @@ const UserTypeSelectionStep = ({ userType, onSelectUserType, userTypeSelections
|
||||
</ItemMedia>
|
||||
<ItemContent className="w-full">
|
||||
<ItemTitle>{option.title}</ItemTitle>
|
||||
<ItemDescription>{option.description}</ItemDescription>
|
||||
<ItemDescription>
|
||||
{option.description}
|
||||
{option.learnMoreUrl && (
|
||||
<>
|
||||
{" "}
|
||||
<VSCodeLink
|
||||
className="inline"
|
||||
style={{ fontSize: "inherit" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
UiServiceClient.openUrl(
|
||||
StringRequest.create({ value: option.learnMoreUrl }),
|
||||
).catch((err) => console.error("Failed to open learn more link:", err))
|
||||
}}>
|
||||
Learn more
|
||||
</VSCodeLink>
|
||||
</>
|
||||
)}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)
|
||||
@@ -301,8 +319,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)
|
||||
@@ -312,7 +329,7 @@ const OnboardingViewContent = ({ onboardingModels }: { onboardingModels: Onboard
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
|
||||
const models = useMemo(() => getClineUIOnboardingGroups(onboardingModels), [onboardingModels])
|
||||
// ClinePass model IDs (e.g. "cline-pass/glm-5.1") aren't keyed in openRouterModels,
|
||||
// ClinePass model IDs (e.g. "cline-pass/glm-5.2") aren't keyed in openRouterModels,
|
||||
// so resolve their info via the slug-based lookup used by ClinePassProvider.
|
||||
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("getClineUIOnboardingGroups", () => {
|
||||
it("buckets ClinePass models into the clinePass group", () => {
|
||||
const result = getClineUIOnboardingGroups(
|
||||
groupOf([
|
||||
model("cline-pass/glm-5.1", CLINEPASS_GROUP),
|
||||
model("cline-pass/glm-5.2", CLINEPASS_GROUP),
|
||||
model("free-model", "free"),
|
||||
model("anthropic/claude", "frontier"),
|
||||
model("z-ai/glm", "open source"),
|
||||
@@ -31,7 +31,7 @@ describe("getClineUIOnboardingGroups", () => {
|
||||
|
||||
expect(result.clinePass).toHaveLength(1)
|
||||
expect(result.clinePass[0].group).toBe(CLINEPASS_GROUP)
|
||||
expect(result.clinePass[0].models.map((m) => m.id)).toEqual(["cline-pass/glm-5.1"])
|
||||
expect(result.clinePass[0].models.map((m) => m.id)).toEqual(["cline-pass/glm-5.2"])
|
||||
expect(result.free[0].models.map((m) => m.id)).toEqual(["free-model"])
|
||||
expect(result.power.flatMap((g) => g.models.map((m) => m.id))).toEqual(["anthropic/claude", "z-ai/glm"])
|
||||
})
|
||||
@@ -54,7 +54,7 @@ describe("getRecommendedModelsData", () => {
|
||||
{
|
||||
recommended: [],
|
||||
free: [],
|
||||
clinePass: [{ id: "cline-pass/glm-5.1", name: "GLM 5.1", description: "", tags: [] }],
|
||||
clinePass: [{ id: "cline-pass/glm-5.2", name: "GLM 5.2", description: "", tags: [] }],
|
||||
},
|
||||
false,
|
||||
)
|
||||
@@ -67,12 +67,12 @@ describe("getRecommendedModelsData", () => {
|
||||
{
|
||||
recommended: [],
|
||||
free: [],
|
||||
clinePass: [{ id: "cline-pass/glm-5.1", name: "GLM 5.1", description: "", tags: [] }],
|
||||
clinePass: [{ id: "cline-pass/glm-5.2", name: "GLM 5.2", description: "", tags: [] }],
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
expect(result?.clinePass.map((model) => model.id)).toEqual(["cline-pass/glm-5.1"])
|
||||
expect(result?.clinePass.map((model) => model.id)).toEqual(["cline-pass/glm-5.2"])
|
||||
})
|
||||
|
||||
it("keeps classic recommended/free responses when the ClinePass feature flag is disabled", () => {
|
||||
@@ -80,7 +80,7 @@ describe("getRecommendedModelsData", () => {
|
||||
{
|
||||
recommended: [{ id: "anthropic/claude", name: "Claude", description: "", tags: [] }],
|
||||
free: [{ id: "free-model", name: "Free", description: "", tags: [] }],
|
||||
clinePass: [{ id: "cline-pass/glm-5.1", name: "GLM 5.1", description: "", tags: [] }],
|
||||
clinePass: [{ id: "cline-pass/glm-5.2", name: "GLM 5.2", description: "", tags: [] }],
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ type UserTypeSelection = {
|
||||
title: string
|
||||
description: string
|
||||
type: NEW_USER_TYPE
|
||||
learnMoreUrl?: string
|
||||
}
|
||||
|
||||
export const STEP_CONFIG = {
|
||||
@@ -56,9 +57,10 @@ export const STEP_CONFIG = {
|
||||
} as const
|
||||
|
||||
const CLINE_PASS_USER_TYPE_SELECTION: UserTypeSelection = {
|
||||
title: "ClinePass (Recommended)",
|
||||
description: "One subscription, curated models, no API keys",
|
||||
title: "ClinePass",
|
||||
description: "Low cost subscription plan for best open weights model.",
|
||||
type: NEW_USER_TYPE.CLINE_PASS,
|
||||
learnMoreUrl: "https://docs.cline.bot/getting-started/clinepass",
|
||||
}
|
||||
|
||||
const BASE_USER_TYPE_SELECTIONS: UserTypeSelection[] = [
|
||||
@@ -69,10 +71,9 @@ const BASE_USER_TYPE_SELECTIONS: UserTypeSelection[] = [
|
||||
|
||||
/**
|
||||
* Returns the onboarding user-type options. The free option leads the list and is
|
||||
* the default selection; ClinePass is inserted as a recommended-but-optional
|
||||
* choice (labeled "Recommended") right after it, only when the `ext-cline-pass`
|
||||
* feature flag is enabled. When the flag is off, the classic Free / Frontier /
|
||||
* BYOK options are shown unchanged.
|
||||
* the default selection; ClinePass is inserted right after it, only when the
|
||||
* `ext-cline-pass` feature flag is enabled. When the flag is off, the classic
|
||||
* Free / Frontier / BYOK options are shown unchanged.
|
||||
*/
|
||||
export function getUserTypeSelections(isClinePassEnabled: boolean): UserTypeSelection[] {
|
||||
if (!isClinePassEnabled) {
|
||||
|
||||
@@ -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()
|
||||
@@ -91,7 +88,7 @@ export function useOnboardingModels(): UseOnboardingModelsResult {
|
||||
return { ...openRouterModels, ...(clineModels ?? {}) }
|
||||
}, [openRouterModels, clineModels])
|
||||
|
||||
// ClinePass model IDs omit the upstream lab (e.g. "cline-pass/glm-5.1"), so look up
|
||||
// ClinePass model IDs omit the upstream lab (e.g. "cline-pass/glm-5.2"), so look up
|
||||
// capabilities via the model slug against the OpenRouter catalog, falling back to
|
||||
// conservative ClinePass defaults. Mirrors ClinePassProvider's resolution.
|
||||
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
|
||||
|
||||
@@ -9,9 +9,7 @@ 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"
|
||||
@@ -21,7 +19,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"
|
||||
@@ -102,9 +99,8 @@ const ApiOptions = ({
|
||||
}: ApiOptionsProps) => {
|
||||
// Use full context state for immediate save payload
|
||||
const { apiConfiguration, remoteConfigSettings } = useExtensionState()
|
||||
const isClinePassEnabled = useHasFeatureFlag(CLINE_PASS_FEATURE_FLAG)
|
||||
|
||||
const { selectedProvider } = normalizeApiConfiguration(apiConfiguration, currentMode, { isClinePassEnabled })
|
||||
const { selectedProvider } = normalizeApiConfiguration(apiConfiguration, currentMode, { isClinePassEnabled: true })
|
||||
|
||||
const { handleModeFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
@@ -145,9 +141,6 @@ const ApiOptions = ({
|
||||
|
||||
const providerOptions = useMemo(() => {
|
||||
let providers = PROVIDERS.list
|
||||
if (!isClinePassEnabled) {
|
||||
providers = providers.filter((option) => option.value !== "cline-pass")
|
||||
}
|
||||
// Filter by platform
|
||||
if (PLATFORM_CONFIG.type !== PlatformType.VSCODE) {
|
||||
// Don't include VS Code LM API for non-VSCode platforms
|
||||
@@ -157,15 +150,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 =
|
||||
remoteProviders.includes("cline-pass") && !remoteProviders.includes("cline")
|
||||
? [...remoteProviders, "cline"]
|
||||
: remoteProviders
|
||||
providers = providers.filter((option) => effectiveRemoteProviders.includes(option.value))
|
||||
}
|
||||
|
||||
return providers
|
||||
}, [isClinePassEnabled, remoteConfigSettings])
|
||||
}, [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 +179,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 +374,17 @@ const ApiOptions = ({
|
||||
<HicapProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
|
||||
)}
|
||||
|
||||
{apiConfiguration && selectedProvider === "cline" && (
|
||||
{apiConfiguration && (selectedProvider === "cline" || selectedProvider === "cline-pass") && (
|
||||
<ClineProvider
|
||||
currentMode={currentMode}
|
||||
initialModelTab={initialModelTab}
|
||||
isClinePassEnabled={isClinePassEnabled}
|
||||
isClinePassEnabled={true}
|
||||
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>
|
||||
|
||||
@@ -47,6 +47,13 @@ const StarIcon = ({ isFavorite, onClick }: { isFavorite: boolean; onClick: (e: R
|
||||
)
|
||||
}
|
||||
|
||||
export interface FeaturedModelTab {
|
||||
label: string
|
||||
models: FeaturedModelCardEntry[]
|
||||
// Optional explanatory copy shown between the tab bar and the model cards
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface ClineModelPickerProps {
|
||||
isPopup?: boolean
|
||||
currentMode: Mode
|
||||
@@ -58,12 +65,17 @@ export interface ClineModelPickerProps {
|
||||
models?: Record<string, ModelInfo>
|
||||
isClinePassEnabled?: boolean
|
||||
showFeaturedModels?: boolean
|
||||
// Custom featured tabs (e.g. ClinePass "Subscribed"/"Free") shown instead of the
|
||||
// built-in Recommended/Free tabs
|
||||
featuredTabs?: FeaturedModelTab[]
|
||||
}
|
||||
|
||||
interface FeaturedModelCardEntry {
|
||||
export interface FeaturedModelCardEntry {
|
||||
id: string
|
||||
description: string
|
||||
label: string
|
||||
// Shown on the card instead of the id (e.g. ClinePass ids without their prefix)
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
const CLINE_RECOMMENDED_MODELS_RETRY_DELAY_MS = 5000
|
||||
@@ -72,7 +84,7 @@ function normalizeModelId(modelId: string): string {
|
||||
return modelId.trim().toLowerCase()
|
||||
}
|
||||
|
||||
function toFeaturedModelCardEntry(
|
||||
export function toFeaturedModelCardEntry(
|
||||
model: Pick<ClineRecommendedModel, "id" | "description" | "tags">,
|
||||
fallbackLabel: string,
|
||||
): FeaturedModelCardEntry | null {
|
||||
@@ -109,6 +121,7 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
models,
|
||||
isClinePassEnabled = true,
|
||||
showFeaturedModels = true,
|
||||
featuredTabs,
|
||||
}) => {
|
||||
const { handleModeFieldsChange, handleFieldChange } = useApiConfigurationHandlers()
|
||||
const { apiConfiguration, favoritedModelIds, clineModels, openRouterModels, refreshClineModels } = useExtensionState()
|
||||
@@ -150,6 +163,7 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
[freeClineModelIds],
|
||||
)
|
||||
const [activeTab, setActiveTab] = useState<"recommended" | "free">(initialTab ?? "recommended")
|
||||
const [activeFeaturedTabIndex, setActiveFeaturedTabIndex] = useState(0)
|
||||
const recommendedModels = useMemo(
|
||||
() => (clineRecommendedModels.length > 0 ? clineRecommendedModels : RECOMMENDED_MODELS_FALLBACK),
|
||||
[clineRecommendedModels],
|
||||
@@ -230,11 +244,26 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
const currentModelId = resolveModelId(configuredModelId)
|
||||
setActiveTab(freeClineModelIdSet.has(normalizeModelId(currentModelId)) ? "free" : "recommended")
|
||||
}, [configuredModelId, freeClineModelIdSet, initialTab, resolveModelId])
|
||||
|
||||
// Keep the active custom tab on the tab containing the configured model
|
||||
useEffect(() => {
|
||||
if (!featuredTabs) {
|
||||
return
|
||||
}
|
||||
const currentModelId = resolveModelId(configuredModelId)
|
||||
const tabIndex = featuredTabs.findIndex((tab) => tab.models.some((model) => model.id === currentModelId))
|
||||
if (tabIndex >= 0) {
|
||||
setActiveFeaturedTabIndex(tabIndex)
|
||||
}
|
||||
}, [configuredModelId, featuredTabs, resolveModelId])
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
const handleModelChange = (rawModelId: string) => {
|
||||
// When a fixed models map is provided (ClinePass), only ids in the map may be
|
||||
// stored — otherwise the host would send arbitrary typed text to the API.
|
||||
const newModelId = models && !(rawModelId in models) ? resolveModelId(rawModelId) : rawModelId
|
||||
setSearchTerm(newModelId)
|
||||
|
||||
handleModeFieldsChange(
|
||||
@@ -414,6 +443,8 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
Object.entries(resolvedModels ?? {})?.some(([id, m]) => id === selectedModelId && m.thinkingConfig) ||
|
||||
selectedModelIdLower.includes("claude-haiku-4.5") ||
|
||||
selectedModelIdLower.includes("claude-4.5-haiku") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-5") ||
|
||||
selectedModelIdLower.includes("claude-5-sonnet") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-4.6") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-4-6") ||
|
||||
selectedModelIdLower.includes("claude-4.6-sonnet") ||
|
||||
@@ -442,7 +473,52 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
|
||||
{showFeaturedModels && (
|
||||
{showFeaturedModels && featuredTabs && (
|
||||
<>
|
||||
{/* Custom Tabs (e.g. ClinePass Subscribed/Free) */}
|
||||
<TabsContainer style={{ marginTop: 4 }}>
|
||||
{featuredTabs.map((tab, index) => (
|
||||
<Tab
|
||||
active={activeFeaturedTabIndex === index}
|
||||
key={tab.label}
|
||||
onClick={() => setActiveFeaturedTabIndex(index)}>
|
||||
{tab.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabsContainer>
|
||||
|
||||
{/* Tab Description */}
|
||||
{featuredTabs[activeFeaturedTabIndex]?.description && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
margin: "4px 0 6px 0",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
{featuredTabs[activeFeaturedTabIndex].description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Model Cards */}
|
||||
<div style={{ marginBottom: "6px" }}>
|
||||
{featuredTabs[activeFeaturedTabIndex]?.models.map((model) => (
|
||||
<FeaturedModelCard
|
||||
description={model.description}
|
||||
isSelected={selectedModelId === model.id}
|
||||
key={model.id}
|
||||
label={model.label}
|
||||
modelId={model.displayName ?? model.id}
|
||||
onClick={() => {
|
||||
handleModelChange(model.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showFeaturedModels && !featuredTabs && (
|
||||
<>
|
||||
{/* Tabs */}
|
||||
<TabsContainer style={{ marginTop: 4 }}>
|
||||
@@ -570,6 +646,14 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{/* Context window switcher for Claude Sonnet 5 */}
|
||||
<ContextWindowSwitcher
|
||||
base1mModelId={`anthropic/claude-sonnet-5${CLAUDE_SONNET_1M_SUFFIX}`}
|
||||
base200kModelId="anthropic/claude-sonnet-5"
|
||||
onModelChange={handleModelChange}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{/* Context window switcher for Claude Opus 4.8 */}
|
||||
<ContextWindowSwitcher
|
||||
base1mModelId={`anthropic/claude-opus-4.8${CLAUDE_SONNET_1M_SUFFIX}`}
|
||||
@@ -654,8 +738,7 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
marginTop: 0,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
The extension automatically fetches the latest Cline model list. If you're unsure which model to choose, Cline
|
||||
works best with <strong>anthropic/claude-sonnet-4.5</strong>.
|
||||
The extension automatically fetches the latest Cline model list.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -221,6 +221,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
|
||||
Object.entries(openRouterModels)?.some(([id, m]) => id === selectedModelId && m.thinkingConfig) ||
|
||||
selectedModelIdLower.includes("claude-haiku-4.5") ||
|
||||
selectedModelIdLower.includes("claude-4.5-haiku") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-5") ||
|
||||
selectedModelIdLower.includes("claude-5-sonnet") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-4.6") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-4-6") ||
|
||||
selectedModelIdLower.includes("claude-4.6-sonnet") ||
|
||||
@@ -331,6 +333,14 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{/* Context window switcher for Claude Sonnet 5 */}
|
||||
<ContextWindowSwitcher
|
||||
base1mModelId={`anthropic/claude-sonnet-5${CLAUDE_SONNET_1M_SUFFIX}`}
|
||||
base200kModelId="anthropic/claude-sonnet-5"
|
||||
onModelChange={handleModelChange}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{/* Context window switcher for Claude Opus 4.8 */}
|
||||
<ContextWindowSwitcher
|
||||
base1mModelId={`anthropic/claude-opus-4.8${CLAUDE_SONNET_1M_SUFFIX}`}
|
||||
@@ -421,9 +431,9 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
|
||||
</VSCodeLink>
|
||||
If you're unsure which model to choose, Cline works best with{" "}
|
||||
<VSCodeLink
|
||||
onClick={() => handleModelChange("anthropic/claude-sonnet-4.6")}
|
||||
onClick={() => handleModelChange("anthropic/claude-sonnet-5")}
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
anthropic/claude-sonnet-4.6.
|
||||
anthropic/claude-sonnet-5.
|
||||
</VSCodeLink>
|
||||
You can also try searching "free" for no-cost options currently available.
|
||||
</p>
|
||||
|
||||
@@ -179,6 +179,8 @@ const VercelModelPicker: React.FC<VercelModelPickerProps> = ({ isPopup, currentM
|
||||
return (
|
||||
selectedModelIdLower.includes("claude-haiku-4.5") ||
|
||||
selectedModelIdLower.includes("claude-4.5-haiku") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-5") ||
|
||||
selectedModelIdLower.includes("claude-5-sonnet") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-4.6") ||
|
||||
selectedModelIdLower.includes("claude-sonnet-4-6") ||
|
||||
selectedModelIdLower.includes("claude-4.6-sonnet") ||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,6 +15,8 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
|
||||
|
||||
// Anthropic models that support thinking/reasoning mode
|
||||
export const SUPPORTED_ANTHROPIC_THINKING_MODELS = [
|
||||
"claude-sonnet-5",
|
||||
`claude-sonnet-5${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
"claude-sonnet-4-6",
|
||||
`claude-sonnet-4-6${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
"claude-3-7-sonnet-20250219",
|
||||
@@ -90,6 +92,14 @@ export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: An
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{/* Context window switcher for Claude Sonnet 5 */}
|
||||
<ContextWindowSwitcher
|
||||
base1mModelId={`claude-sonnet-5${CLAUDE_SONNET_1M_SUFFIX}`}
|
||||
base200kModelId="claude-sonnet-5"
|
||||
onModelChange={handleModelChange}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{/* Context window switcher for Claude Opus 4.6 */}
|
||||
<ContextWindowSwitcher
|
||||
base1mModelId={`claude-opus-4-6${CLAUDE_SONNET_1M_SUFFIX}`}
|
||||
|
||||
@@ -24,6 +24,8 @@ import { getModeSpecificFields, normalizeApiConfiguration } from "../utils/provi
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
|
||||
export const SUPPORTED_BEDROCK_THINKING_MODELS = [
|
||||
"anthropic.claude-sonnet-5",
|
||||
`anthropic.claude-sonnet-5${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
"anthropic.claude-sonnet-4-6",
|
||||
`anthropic.claude-sonnet-4-6${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
"anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
|
||||
@@ -13,6 +13,7 @@ const SUPPORTED_CLAUDE_CODE_THINKING_MODELS = [
|
||||
...SUPPORTED_ANTHROPIC_THINKING_MODELS,
|
||||
"sonnet",
|
||||
"sonnet[1m]",
|
||||
"claude-sonnet-5[1m]",
|
||||
"claude-fable-5[1m]",
|
||||
"claude-opus-4-8[1m]",
|
||||
"claude-opus-4-7[1m]",
|
||||
|
||||
@@ -1,46 +1,89 @@
|
||||
import {
|
||||
buildModelInfoNameMap,
|
||||
clinePassDefaultModelId,
|
||||
clinePassModelInfoSaneDefaults,
|
||||
clinePassModels,
|
||||
type ModelInfo,
|
||||
resolveClinePassModelInfo,
|
||||
} from "@shared/api"
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@shared/cline/recommended-models"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import type { ClineRecommendedModel } from "@shared/proto/cline/models"
|
||||
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"
|
||||
import ClineModelPicker, {
|
||||
type FeaturedModelCardEntry,
|
||||
type FeaturedModelTab,
|
||||
toFeaturedModelCardEntry,
|
||||
} from "../ClineModelPicker"
|
||||
import { getModeSpecificFields } from "../utils/providerUtils"
|
||||
|
||||
export const ClinePassProvider: typeof ClineProvider = (props) => {
|
||||
const { openRouterModels } = useExtensionState()
|
||||
interface ClinePassProviderProps {
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
currentMode: Mode
|
||||
showAccountCard?: boolean
|
||||
}
|
||||
|
||||
const CLINE_PASS_MODEL_FIELD_PAIRS = {
|
||||
modelId: { plan: "planModeClinePassModelId", act: "actModeClinePassModelId" },
|
||||
modelInfo: { plan: "planModeClinePassModelInfo", act: "actModeClinePassModelInfo" },
|
||||
} as const
|
||||
|
||||
const FREE_TAB_DESCRIPTION =
|
||||
"Try these models with limited free usage, included at no cost and separate from your ClinePass quota."
|
||||
|
||||
function zeroPriced(info: ModelInfo): ModelInfo {
|
||||
return {
|
||||
...info,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const ClinePassProvider = ({
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
currentMode,
|
||||
showAccountCard = true,
|
||||
}: ClinePassProviderProps) => {
|
||||
const { apiConfiguration, openRouterModels, clineModels, refreshClineModels } = useExtensionState()
|
||||
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
|
||||
const [clinePassRawModels, setClinePassRawModels] = useState<ClineRecommendedModel[]>([])
|
||||
const [clinePassRecommendedModels, setClinePassRecommendedModels] = useState<Record<string, ModelInfo> | undefined>(undefined)
|
||||
const [clineFreeModels, setClineFreeModels] = useState<ClineRecommendedModel[]>([])
|
||||
|
||||
const refreshClinePassModels = useCallback(async () => {
|
||||
try {
|
||||
const response = await ModelsServiceClient.refreshClineRecommendedModelsRpc(EmptyRequest.create({}))
|
||||
const clinePassResponseModels = (response.clinePass ?? []).filter((model) => model.id)
|
||||
const models = Object.fromEntries(
|
||||
(response.clinePass ?? [])
|
||||
.filter((model) => model.id)
|
||||
.map((model) => {
|
||||
// ClinePass model IDs omit the upstream lab, so look up capabilities using
|
||||
// the model slug (for example, glm-5.1 instead of cline-pass/glm-5.1).
|
||||
// If the model is not in OpenRouter yet, use conservative generic defaults
|
||||
// instead of copying GLM-5.1-specific context/max-token values.
|
||||
const fallback = resolveClinePassModelInfo(model.id, openRouterModelsByName)
|
||||
return [
|
||||
model.id,
|
||||
{
|
||||
...fallback,
|
||||
name: model.name || fallback.name || model.id,
|
||||
description: model.description || fallback.description,
|
||||
},
|
||||
]
|
||||
}),
|
||||
clinePassResponseModels.map((model) => {
|
||||
// ClinePass model IDs omit the upstream lab, so look up capabilities using
|
||||
// the model slug (for example, glm-5.2 instead of cline-pass/glm-5.2).
|
||||
// If the model is not in OpenRouter yet, use conservative generic defaults
|
||||
// instead of copying GLM-5.2-specific context/max-token values.
|
||||
const fallback = resolveClinePassModelInfo(model.id, openRouterModelsByName)
|
||||
return [
|
||||
model.id,
|
||||
{
|
||||
...fallback,
|
||||
name: model.name || fallback.name || model.id,
|
||||
// The info panel shows the full catalog description; the endpoint's
|
||||
// short blurb is only for the featured cards
|
||||
description: fallback.description || model.description,
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
setClinePassRawModels(clinePassResponseModels)
|
||||
setClinePassRecommendedModels(Object.keys(models).length > 0 ? models : undefined)
|
||||
setClineFreeModels((response.free ?? []).filter((model) => model.id))
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh ClinePass models:", error)
|
||||
}
|
||||
@@ -50,7 +93,54 @@ export const ClinePassProvider: typeof ClineProvider = (props) => {
|
||||
void refreshClinePassModels()
|
||||
}, [refreshClinePassModels])
|
||||
|
||||
const clinePassModelOptions = clinePassRecommendedModels ?? clinePassModels
|
||||
// The picker skips its own catalog refresh when a models map is provided, but the
|
||||
// free-model entries below resolve their capabilities from the cline catalog.
|
||||
useEffect(() => {
|
||||
refreshClineModels()
|
||||
}, [refreshClineModels])
|
||||
|
||||
const freeRecommendedModels = useMemo(
|
||||
() => (clineFreeModels.length > 0 ? clineFreeModels : CLINE_RECOMMENDED_MODELS_FALLBACK.free),
|
||||
[clineFreeModels],
|
||||
)
|
||||
|
||||
// Free models are OpenRouter-style ids billed at $0, so resolve their info by full id
|
||||
// from the dynamic catalogs and store them zero-priced.
|
||||
const freeModelEntries = useMemo(() => {
|
||||
const modelCatalog: Record<string, ModelInfo> = { ...openRouterModels, ...clineModels }
|
||||
return Object.fromEntries(
|
||||
freeRecommendedModels
|
||||
.filter((model) => model.id)
|
||||
.map((model) => {
|
||||
const base = modelCatalog[model.id] ?? clinePassModelInfoSaneDefaults
|
||||
return [
|
||||
model.id,
|
||||
zeroPriced({
|
||||
...base,
|
||||
name: model.name || base.name || model.id,
|
||||
// The info panel shows the full catalog description; the endpoint's
|
||||
// short blurb is only for the featured cards
|
||||
description: base.description || model.description,
|
||||
}),
|
||||
]
|
||||
}),
|
||||
)
|
||||
}, [freeRecommendedModels, openRouterModels, clineModels])
|
||||
|
||||
const { clinePassModelId: configuredClinePassModelId, clinePassModelInfo: configuredClinePassModelInfo } =
|
||||
getModeSpecificFields(apiConfiguration, currentMode)
|
||||
|
||||
const clinePassModelOptions = useMemo(() => {
|
||||
// ClinePass entries first so the default-model fallback below stays a pass model.
|
||||
const merged: Record<string, ModelInfo> = { ...(clinePassRecommendedModels ?? clinePassModels), ...freeModelEntries }
|
||||
// Keep a previously selected model visible even if it later drops out of the
|
||||
// endpoint's buckets, so the picker doesn't display a model the host won't send.
|
||||
if (configuredClinePassModelId && !(configuredClinePassModelId in merged) && configuredClinePassModelInfo) {
|
||||
merged[configuredClinePassModelId] = configuredClinePassModelInfo
|
||||
}
|
||||
return merged
|
||||
}, [clinePassRecommendedModels, freeModelEntries, configuredClinePassModelId, configuredClinePassModelInfo])
|
||||
|
||||
const clinePassDefaultModel = useMemo(() => {
|
||||
if (!clinePassModelOptions) {
|
||||
return undefined
|
||||
@@ -61,20 +151,59 @@ export const ClinePassProvider: typeof ClineProvider = (props) => {
|
||||
: (Object.keys(clinePassModelOptions)[0] ?? clinePassDefaultModelId)
|
||||
}, [clinePassModelOptions])
|
||||
|
||||
// Subscription models show their endpoint description but no label chip — the
|
||||
// whole list is included with the plan, so a per-card tag adds nothing
|
||||
const subscribedModelCards = useMemo<FeaturedModelCardEntry[]>(() => {
|
||||
const models =
|
||||
clinePassRawModels.length > 0
|
||||
? clinePassRawModels.map((model) => ({ id: model.id, description: model.description }))
|
||||
: Object.entries(clinePassRecommendedModels ?? clinePassModels).map(([id, info]) => ({
|
||||
id,
|
||||
description: info.description,
|
||||
}))
|
||||
return models.map((model) => ({
|
||||
id: model.id,
|
||||
displayName: model.id.replace(/^cline-pass\//, ""),
|
||||
label: "",
|
||||
description: model.description || "",
|
||||
}))
|
||||
}, [clinePassRawModels, clinePassRecommendedModels])
|
||||
|
||||
const freeModelCards = useMemo(
|
||||
() =>
|
||||
freeRecommendedModels
|
||||
.map((model) => toFeaturedModelCardEntry(model, "FREE"))
|
||||
.filter((model): model is FeaturedModelCardEntry => model !== null),
|
||||
[freeRecommendedModels],
|
||||
)
|
||||
|
||||
const featuredTabs = useMemo<FeaturedModelTab[]>(() => {
|
||||
const tabs: FeaturedModelTab[] = [{ label: "Subscribed", models: subscribedModelCards }]
|
||||
if (freeModelCards.length > 0) {
|
||||
tabs.push({ label: "Free", models: freeModelCards, description: FREE_TAB_DESCRIPTION })
|
||||
}
|
||||
return tabs
|
||||
}, [subscribedModelCards, freeModelCards])
|
||||
|
||||
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}
|
||||
featuredTabs={featuredTabs}
|
||||
isPopup={isPopup}
|
||||
modelIdFieldPair={CLINE_PASS_MODEL_FIELD_PAIRS.modelId}
|
||||
modelInfoFieldPair={CLINE_PASS_MODEL_FIELD_PAIRS.modelInfo}
|
||||
models={clinePassModelOptions}
|
||||
/>
|
||||
)}
|
||||
</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 is a low cost subscription plan including usage of 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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -25,6 +25,8 @@ interface VertexProviderProps {
|
||||
|
||||
// Vertex models that support thinking
|
||||
const SUPPORTED_THINKING_MODELS = [
|
||||
"claude-sonnet-5",
|
||||
"claude-sonnet-5:1m",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-sonnet-4-6:1m",
|
||||
"claude-haiku-4-5@20251001",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { ApiConfiguration, ModelInfo } from "@shared/api"
|
||||
import { clinePassDefaultModelId, clinePassModels } from "@shared/api"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { normalizeApiConfiguration, syncModeConfigurations } from "../providerUtils"
|
||||
|
||||
describe("providerUtils", () => {
|
||||
const freeModelInfo: ModelInfo = {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 256_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
description: "A free model",
|
||||
}
|
||||
|
||||
describe("normalizeApiConfiguration cline-pass", () => {
|
||||
it("passes a free (non cline-pass prefixed) model id through with its stored info", () => {
|
||||
const apiConfiguration: ApiConfiguration = {
|
||||
actModeApiProvider: "cline-pass",
|
||||
actModeClinePassModelId: "kwaipilot/kat-coder-pro",
|
||||
actModeClinePassModelInfo: freeModelInfo,
|
||||
}
|
||||
|
||||
const normalized = normalizeApiConfiguration(apiConfiguration, "act", { isClinePassEnabled: true })
|
||||
expect(normalized.selectedProvider).toBe("cline-pass")
|
||||
expect(normalized.selectedModelId).toBe("kwaipilot/kat-coder-pro")
|
||||
expect(normalized.selectedModelInfo).toEqual(freeModelInfo)
|
||||
})
|
||||
|
||||
it("passes a :free suffixed model id through with its stored info", () => {
|
||||
const apiConfiguration: ApiConfiguration = {
|
||||
actModeApiProvider: "cline-pass",
|
||||
actModeClinePassModelId: "arcee-ai/trinity-large-preview:free",
|
||||
actModeClinePassModelInfo: freeModelInfo,
|
||||
}
|
||||
|
||||
const normalized = normalizeApiConfiguration(apiConfiguration, "act", { isClinePassEnabled: true })
|
||||
expect(normalized.selectedModelId).toBe("arcee-ai/trinity-large-preview:free")
|
||||
expect(normalized.selectedModelInfo).toEqual(freeModelInfo)
|
||||
})
|
||||
|
||||
it("keeps cline-pass prefixed ids resolved against the static model table", () => {
|
||||
const apiConfiguration: ApiConfiguration = {
|
||||
actModeApiProvider: "cline-pass",
|
||||
actModeClinePassModelId: "cline-pass/glm-5.2",
|
||||
}
|
||||
|
||||
const normalized = normalizeApiConfiguration(apiConfiguration, "act", { isClinePassEnabled: true })
|
||||
expect(normalized.selectedModelId).toBe("cline-pass/glm-5.2")
|
||||
expect(normalized.selectedModelInfo).toEqual(clinePassModels["cline-pass/glm-5.2"])
|
||||
})
|
||||
|
||||
it("falls back to the default pass model when no model id is configured", () => {
|
||||
const apiConfiguration: ApiConfiguration = {
|
||||
actModeApiProvider: "cline-pass",
|
||||
}
|
||||
|
||||
const normalized = normalizeApiConfiguration(apiConfiguration, "act", { isClinePassEnabled: true })
|
||||
expect(normalized.selectedModelId).toBe(clinePassDefaultModelId)
|
||||
expect(normalized.selectedModelInfo).toEqual(clinePassModels[clinePassDefaultModelId])
|
||||
})
|
||||
})
|
||||
|
||||
describe("syncModeConfigurations cline-pass", () => {
|
||||
it("copies a free model id and info across plan and act modes", async () => {
|
||||
const apiConfiguration: ApiConfiguration = {
|
||||
planModeApiProvider: "cline-pass",
|
||||
actModeApiProvider: "cline-pass",
|
||||
actModeClinePassModelId: "kwaipilot/kat-coder-pro",
|
||||
actModeClinePassModelInfo: freeModelInfo,
|
||||
}
|
||||
|
||||
let updates: Partial<ApiConfiguration> | undefined
|
||||
await syncModeConfigurations(apiConfiguration, "act", async (fields) => {
|
||||
updates = fields
|
||||
})
|
||||
|
||||
expect(updates?.planModeClinePassModelId).toBe("kwaipilot/kat-coder-pro")
|
||||
expect(updates?.planModeClinePassModelInfo).toEqual(freeModelInfo)
|
||||
expect(updates?.actModeClinePassModelId).toBe("kwaipilot/kat-coder-pro")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
basetenModels,
|
||||
bedrockDefaultModelId,
|
||||
bedrockModels,
|
||||
buildModelInfoNameMap,
|
||||
cerebrasDefaultModelId,
|
||||
cerebrasModels,
|
||||
claudeCodeDefaultModelId,
|
||||
@@ -354,23 +355,25 @@ export function normalizeApiConfiguration(
|
||||
currentMode === "plan"
|
||||
? apiConfiguration?.planModeClinePassModelId
|
||||
: apiConfiguration?.actModeClinePassModelId;
|
||||
const clinePassModelId = configuredClinePassModelId?.startsWith(
|
||||
"cline-pass/",
|
||||
)
|
||||
? configuredClinePassModelId
|
||||
: clinePassDefaultModelId;
|
||||
// ClinePass users may also select Cline free models (OpenRouter-style ids
|
||||
// without the cline-pass/ prefix), so pass any configured id through.
|
||||
const clinePassModelId =
|
||||
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": {
|
||||
|
||||
@@ -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"
|
||||
@@ -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,4 +1,5 @@
|
||||
import { ApiConfiguration, clinePassDefaultModelId, clinePassModels, ModelInfo, openRouterDefaultModelId } from "@shared/api"
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@shared/cline/recommended-models"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { getModeSpecificFields } from "@/components/settings/utils/providerUtils"
|
||||
|
||||
@@ -222,7 +223,10 @@ export function validateModelId(
|
||||
}
|
||||
if (
|
||||
!Object.keys(clinePassModels).includes(clinePassResolvedModelId) &&
|
||||
!clinePassResolvedModelId.startsWith("cline-pass/")
|
||||
!clinePassResolvedModelId.startsWith("cline-pass/") &&
|
||||
// ClinePass users may also select Cline free models (OpenRouter-style ids)
|
||||
!(clineModels && Object.keys(clineModels).includes(clinePassResolvedModelId)) &&
|
||||
!CLINE_RECOMMENDED_MODELS_FALLBACK.free.some((model) => model.id === clinePassResolvedModelId)
|
||||
) {
|
||||
return "The model ID you provided is not available. Please choose a different model."
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user