mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2cfdd37d | |||
| 16a00b1bb2 | |||
| 496151de4a | |||
| 9a51359f5b | |||
| 39a21d9480 | |||
| 17c5cc0062 | |||
| dfc9bb8493 | |||
| 18b06dbc6a | |||
| 6510f6898c | |||
| ce80bdfcbc | |||
| 7c3b154ae2 | |||
| f14b0c9c00 | |||
| 951bfe97a7 | |||
| 3d0968f2e0 |
@@ -127,6 +127,7 @@ message ClineRecommendedModel {
|
||||
message ClineRecommendedModelsResponse {
|
||||
repeated ClineRecommendedModel recommended = 1;
|
||||
repeated ClineRecommendedModel free = 2;
|
||||
repeated ClineRecommendedModel cline_pass = 3;
|
||||
}
|
||||
|
||||
// Request for fetching OpenAI models
|
||||
@@ -287,6 +288,8 @@ message ModelsApiOptions {
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 134;
|
||||
optional string plan_mode_cline_model_id = 135;
|
||||
optional OpenRouterModelInfo plan_mode_cline_model_info = 136;
|
||||
optional string plan_mode_cline_pass_model_id = 137;
|
||||
optional OpenRouterModelInfo plan_mode_cline_pass_model_info = 138;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -326,6 +329,8 @@ message ModelsApiOptions {
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 234;
|
||||
optional string act_mode_cline_model_id = 235;
|
||||
optional OpenRouterModelInfo act_mode_cline_model_info = 236;
|
||||
optional string act_mode_cline_pass_model_id = 237;
|
||||
optional OpenRouterModelInfo act_mode_cline_pass_model_info = 238;
|
||||
}
|
||||
|
||||
// Request for updating API configuration (legacy - uses combined configuration)
|
||||
@@ -461,6 +466,7 @@ enum ApiProvider {
|
||||
NOUSRESEARCH = 39;
|
||||
OPENAI_CODEX = 40;
|
||||
WANDB = 41;
|
||||
CLINE_PASS = 42;
|
||||
}
|
||||
|
||||
enum ApiFormat {
|
||||
@@ -644,6 +650,8 @@ message ModelsApiConfiguration {
|
||||
optional string gemini_plan_mode_thinking_level = 139;
|
||||
optional string plan_mode_cline_model_id = 140;
|
||||
optional OpenRouterModelInfo plan_mode_cline_model_info = 141;
|
||||
optional string plan_mode_cline_pass_model_id = 142;
|
||||
optional OpenRouterModelInfo plan_mode_cline_pass_model_info = 143;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -688,4 +696,6 @@ message ModelsApiConfiguration {
|
||||
optional string gemini_act_mode_thinking_level = 239;
|
||||
optional string act_mode_cline_model_id = 240;
|
||||
optional OpenRouterModelInfo act_mode_cline_model_info = 241;
|
||||
optional string act_mode_cline_pass_model_id = 242;
|
||||
optional OpenRouterModelInfo act_mode_cline_pass_model_info = 243;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
|
||||
import { ApiConfiguration, 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"
|
||||
@@ -78,7 +80,10 @@ function createHandlerForProvider(
|
||||
options: Omit<ApiConfiguration, "apiProvider">,
|
||||
mode: Mode,
|
||||
): ApiHandler {
|
||||
switch (apiProvider) {
|
||||
const effectiveApiProvider =
|
||||
apiProvider === "cline-pass" && !featureFlagsService.getBooleanFlagEnabled(FeatureFlag.CLINE_PASS) ? "cline" : apiProvider
|
||||
|
||||
switch (effectiveApiProvider) {
|
||||
case "anthropic":
|
||||
return new AnthropicHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
@@ -258,11 +263,12 @@ function createHandlerForProvider(
|
||||
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
|
||||
})
|
||||
case "cline": {
|
||||
const configuredClineModelId = mode === "plan" ? options.planModeClineModelId : options.actModeClineModelId
|
||||
const configuredClineModelInfo = mode === "plan" ? options.planModeClineModelInfo : options.actModeClineModelInfo
|
||||
const clineModelId =
|
||||
(mode === "plan" ? options.planModeClineModelId : options.actModeClineModelId) ||
|
||||
(mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId)
|
||||
configuredClineModelId || (mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId)
|
||||
const clineModelInfo =
|
||||
(mode === "plan" ? options.planModeClineModelInfo : options.actModeClineModelInfo) ||
|
||||
configuredClineModelInfo ||
|
||||
(mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo)
|
||||
return new ClineHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
@@ -278,6 +284,29 @@ function createHandlerForProvider(
|
||||
enableParallelToolCalling: options.enableParallelToolCalling,
|
||||
})
|
||||
}
|
||||
case "cline-pass": {
|
||||
const configuredClinePassModelId =
|
||||
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)
|
||||
return new ClineHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
clineAccountId: options.clineAccountId,
|
||||
clineApiKey: options.clineApiKey,
|
||||
ulid: options.ulid,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
openRouterProviderSorting: options.openRouterProviderSorting,
|
||||
openRouterModelId: clineModelId,
|
||||
openRouterModelInfo: clineModelInfo,
|
||||
enableParallelToolCalling: options.enableParallelToolCalling,
|
||||
})
|
||||
}
|
||||
case "litellm":
|
||||
return new LiteLlmHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { clinePassModels, type ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
@@ -37,7 +37,10 @@ function normalizeModelId(modelId: string): string {
|
||||
return modelId.trim().toLowerCase()
|
||||
}
|
||||
|
||||
const CLINE_FREE_MODEL_IDS = new Set(CLINE_RECOMMENDED_MODELS_FALLBACK.free.map((model) => normalizeModelId(model.id)))
|
||||
const CLINE_FREE_MODEL_IDS = new Set([
|
||||
...CLINE_RECOMMENDED_MODELS_FALLBACK.free.map((model) => normalizeModelId(model.id)),
|
||||
...Object.keys(clinePassModels).map((modelId) => normalizeModelId(modelId)),
|
||||
])
|
||||
|
||||
function getCacheReadTokens(usage: any): number {
|
||||
return usage?.prompt_tokens_details?.cached_tokens || usage?.cache_read_input_tokens || 0
|
||||
@@ -67,7 +70,9 @@ export class ClineHandler implements ApiHandler {
|
||||
private async getFreeModelIdSet(): Promise<Set<string>> {
|
||||
try {
|
||||
const models = await refreshClineRecommendedModels()
|
||||
const freeModelIds = models.free.map((model) => normalizeModelId(model.id)).filter((modelId) => modelId.length > 0)
|
||||
const freeModelIds = [...models.free, ...models.clinePass]
|
||||
.map((model) => normalizeModelId(model.id))
|
||||
.filter((modelId) => modelId.length > 0)
|
||||
if (freeModelIds.length > 0) {
|
||||
return new Set(freeModelIds)
|
||||
}
|
||||
|
||||
+82
-39
@@ -1,27 +1,30 @@
|
||||
import * as disk from "@core/storage/disk"
|
||||
import axios from "axios"
|
||||
import { expect } from "chai"
|
||||
import fs from "fs/promises"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import { ClineEnv, Environment } from "@/config"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { refreshClineRecommendedModels, resetClineRecommendedModelsCacheForTests } from "../refreshClineRecommendedModels"
|
||||
import * as disk from "@core/storage/disk";
|
||||
import axios from "axios";
|
||||
import { expect } from "chai";
|
||||
import fs from "fs/promises";
|
||||
import { afterEach, beforeEach, describe, it } from "mocha";
|
||||
import sinon from "sinon";
|
||||
import { ClineEnv, Environment } from "@/config";
|
||||
import { Logger } from "@/shared/services/Logger";
|
||||
import {
|
||||
refreshClineRecommendedModels,
|
||||
resetClineRecommendedModelsCacheForTests,
|
||||
} from "../refreshClineRecommendedModels";
|
||||
|
||||
describe("refreshClineRecommendedModels", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
resetClineRecommendedModelsCacheForTests()
|
||||
sandbox.stub(Logger, "log")
|
||||
sandbox.stub(Logger, "error")
|
||||
})
|
||||
sandbox = sinon.createSandbox();
|
||||
resetClineRecommendedModelsCacheForTests();
|
||||
sandbox.stub(Logger, "log");
|
||||
sandbox.stub(Logger, "error");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetClineRecommendedModelsCacheForTests()
|
||||
sandbox.restore()
|
||||
})
|
||||
resetClineRecommendedModelsCacheForTests();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it("fetches from upstream", async () => {
|
||||
sandbox.stub(ClineEnv, "config").returns({
|
||||
@@ -29,19 +32,32 @@ describe("refreshClineRecommendedModels", () => {
|
||||
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(disk, "ensureCacheDirectoryExists").resolves("/tmp");
|
||||
sandbox.stub(fs, "writeFile").resolves();
|
||||
const axiosGetStub = sandbox.stub(axios, "get").resolves({
|
||||
data: {
|
||||
recommended: [{ id: "anthropic/claude-sonnet-4.6", description: "Remote recommended", tags: ["NEW"] }],
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
description: "Remote recommended",
|
||||
tags: ["NEW"],
|
||||
},
|
||||
],
|
||||
free: [{ id: "z-ai/glm-5", description: "Remote free" }],
|
||||
clinePass: [
|
||||
{
|
||||
id: "cline-pass/glm-5",
|
||||
description: "Remote Cline Pass",
|
||||
tags: ["CLINE_PASS"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const result = await refreshClineRecommendedModels()
|
||||
const result = await refreshClineRecommendedModels();
|
||||
|
||||
expect(axiosGetStub.calledOnce).to.equal(true)
|
||||
expect(axiosGetStub.calledOnce).to.equal(true);
|
||||
expect(result).to.deep.equal({
|
||||
recommended: [
|
||||
{
|
||||
@@ -59,8 +75,16 @@ describe("refreshClineRecommendedModels", () => {
|
||||
tags: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
clinePass: [
|
||||
{
|
||||
id: "cline-pass/glm-5",
|
||||
name: "cline-pass/glm-5",
|
||||
description: "Remote Cline Pass",
|
||||
tags: ["CLINE_PASS"],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the in-memory cache after upstream cache is populated", async () => {
|
||||
sandbox.stub(ClineEnv, "config").returns({
|
||||
@@ -68,20 +92,39 @@ describe("refreshClineRecommendedModels", () => {
|
||||
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(disk, "ensureCacheDirectoryExists").resolves("/tmp");
|
||||
sandbox.stub(fs, "writeFile").resolves();
|
||||
const axiosGetStub = sandbox.stub(axios, "get").resolves({
|
||||
data: {
|
||||
recommended: [{ id: "google/gemini-3.1-pro-preview", description: "Remote recommended", tags: ["NEW"] }],
|
||||
free: [{ id: "minimax/minimax-m2.5", description: "Remote free", tags: ["FREE"] }],
|
||||
recommended: [
|
||||
{
|
||||
id: "google/gemini-3.1-pro-preview",
|
||||
description: "Remote recommended",
|
||||
tags: ["NEW"],
|
||||
},
|
||||
],
|
||||
free: [
|
||||
{
|
||||
id: "minimax/minimax-m2.5",
|
||||
description: "Remote free",
|
||||
tags: ["FREE"],
|
||||
},
|
||||
],
|
||||
clinePass: [
|
||||
{
|
||||
id: "cline-pass/glm-5",
|
||||
description: "Remote Cline Pass",
|
||||
tags: ["CLINE_PASS"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const firstResult = await refreshClineRecommendedModels()
|
||||
const secondResult = await refreshClineRecommendedModels()
|
||||
const firstResult = await refreshClineRecommendedModels();
|
||||
const secondResult = await refreshClineRecommendedModels();
|
||||
|
||||
expect(axiosGetStub.calledOnce).to.equal(true)
|
||||
expect(secondResult).to.deep.equal(firstResult)
|
||||
})
|
||||
})
|
||||
expect(axiosGetStub.calledOnce).to.equal(true);
|
||||
expect(secondResult).to.deep.equal(firstResult);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,12 +16,16 @@ export interface ClineRecommendedModelData {
|
||||
export interface ClineRecommendedModelsData {
|
||||
recommended: ClineRecommendedModelData[]
|
||||
free: ClineRecommendedModelData[]
|
||||
clinePass: ClineRecommendedModelData[]
|
||||
}
|
||||
|
||||
const RECOMMENDED_MODELS_CACHE_TTL_MS = 60 * 60 * 1000
|
||||
|
||||
let pendingRefresh: Promise<ClineRecommendedModelsData> | null = null
|
||||
let inMemoryCache: { data: ClineRecommendedModelsData; timestamp: number } | null = null
|
||||
let inMemoryCache: {
|
||||
data: ClineRecommendedModelsData
|
||||
timestamp: number
|
||||
} | null = null
|
||||
|
||||
function normalizeRecommendedModel(raw: unknown): ClineRecommendedModelData | null {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
@@ -49,13 +53,16 @@ function normalizeRecommendedModelsResponse(raw: unknown): ClineRecommendedModel
|
||||
const data = raw as Record<string, unknown>
|
||||
if (
|
||||
(data.recommended !== undefined && !Array.isArray(data.recommended)) ||
|
||||
(data.free !== undefined && !Array.isArray(data.free))
|
||||
(data.free !== undefined && !Array.isArray(data.free)) ||
|
||||
(data.clinePass !== undefined && !Array.isArray(data.clinePass)) ||
|
||||
(data.cline_pass !== undefined && !Array.isArray(data.cline_pass))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const recommendedRaw = Array.isArray(data.recommended) ? data.recommended : []
|
||||
const freeRaw = Array.isArray(data.free) ? data.free : []
|
||||
const clinePassRaw = Array.isArray(data.clinePass) ? data.clinePass : Array.isArray(data.cline_pass) ? data.cline_pass : []
|
||||
|
||||
const recommended = recommendedRaw
|
||||
.map((model) => normalizeRecommendedModel(model))
|
||||
@@ -65,7 +72,11 @@ function normalizeRecommendedModelsResponse(raw: unknown): ClineRecommendedModel
|
||||
.map((model) => normalizeRecommendedModel(model))
|
||||
.filter((model): model is ClineRecommendedModelData => model !== null)
|
||||
|
||||
return { recommended, free }
|
||||
const clinePass = clinePassRaw
|
||||
.map((model) => normalizeRecommendedModel(model))
|
||||
.filter((model): model is ClineRecommendedModelData => model !== null)
|
||||
|
||||
return { recommended, free, clinePass }
|
||||
}
|
||||
|
||||
export async function refreshClineRecommendedModels(): Promise<ClineRecommendedModelsData> {
|
||||
@@ -95,7 +106,11 @@ export function resetClineRecommendedModelsCacheForTests(): void {
|
||||
|
||||
async function fetchAndCacheClineRecommendedModels(): Promise<ClineRecommendedModelsData> {
|
||||
const clineRecommendedModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.clineRecommendedModels)
|
||||
let result: ClineRecommendedModelsData = { recommended: [], free: [] }
|
||||
let result: ClineRecommendedModelsData = {
|
||||
recommended: [],
|
||||
free: [],
|
||||
clinePass: [],
|
||||
}
|
||||
|
||||
try {
|
||||
const apiBaseUrl = ClineEnv.config().apiBaseUrl
|
||||
@@ -120,7 +135,11 @@ async function fetchAndCacheClineRecommendedModels(): Promise<ClineRecommendedMo
|
||||
const fileContents = await fs.readFile(clineRecommendedModelsFilePath, "utf8")
|
||||
const parsed = JSON.parse(fileContents)
|
||||
if (parsed) {
|
||||
result = parsed
|
||||
result = {
|
||||
recommended: Array.isArray(parsed.recommended) ? parsed.recommended : [],
|
||||
free: Array.isArray(parsed.free) ? parsed.free : [],
|
||||
clinePass: Array.isArray(parsed.clinePass) ? parsed.clinePass : [],
|
||||
}
|
||||
Logger.log("Loaded Cline recommended models from cache")
|
||||
}
|
||||
}
|
||||
@@ -130,7 +149,7 @@ async function fetchAndCacheClineRecommendedModels(): Promise<ClineRecommendedMo
|
||||
}
|
||||
|
||||
// Avoid pinning empty results in memory for the full TTL after a transient API/cache miss.
|
||||
if (result.recommended.length > 0 || result.free.length > 0) {
|
||||
if (result.recommended.length > 0 || result.free.length > 0 || result.clinePass.length > 0) {
|
||||
inMemoryCache = { data: result, timestamp: Date.now() }
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -25,5 +25,13 @@ export async function refreshClineRecommendedModelsRpc(
|
||||
tags: model.tags,
|
||||
}),
|
||||
),
|
||||
clinePass: models.clinePass.map((model) =>
|
||||
ClineRecommendedModel.create({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
description: model.description,
|
||||
tags: model.tags,
|
||||
}),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/cline/models"
|
||||
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { Empty } from "@shared/proto/cline/common";
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/cline/models";
|
||||
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion";
|
||||
import {
|
||||
fromProtobufLiteLLMModelInfo,
|
||||
fromProtobufModelInfo,
|
||||
fromProtobufOcaModelInfo,
|
||||
fromProtobufOpenAiCompatibleModelInfo,
|
||||
} from "@shared/proto-conversions/models/typeConversion"
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
} from "@shared/proto-conversions/models/typeConversion";
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types";
|
||||
import { buildApiHandler } from "@/core/api";
|
||||
import { Logger } from "@/shared/services/Logger";
|
||||
import type { Controller } from "../index";
|
||||
|
||||
/**
|
||||
* Updates API configuration
|
||||
@@ -24,18 +24,22 @@ export async function updateApiConfigurationProto(
|
||||
): Promise<Empty> {
|
||||
try {
|
||||
if (!request.apiConfiguration) {
|
||||
Logger.log("[APICONFIG: updateApiConfigurationProto] API configuration is required")
|
||||
throw new Error("API configuration is required")
|
||||
Logger.log(
|
||||
"[APICONFIG: updateApiConfigurationProto] API configuration is required",
|
||||
);
|
||||
throw new Error("API configuration is required");
|
||||
}
|
||||
|
||||
const protoApiConfiguration = request.apiConfiguration
|
||||
const protoApiConfiguration = request.apiConfiguration;
|
||||
|
||||
const convertedApiConfigurationFromProto = {
|
||||
...protoApiConfiguration,
|
||||
// Convert proto ApiProvider enums to native string types
|
||||
planModeApiProvider:
|
||||
protoApiConfiguration.planModeApiProvider !== undefined
|
||||
? convertProtoToApiProvider(protoApiConfiguration.planModeApiProvider!)
|
||||
? convertProtoToApiProvider(
|
||||
protoApiConfiguration.planModeApiProvider!,
|
||||
)
|
||||
: undefined,
|
||||
actModeApiProvider:
|
||||
protoApiConfiguration.actModeApiProvider !== undefined
|
||||
@@ -44,20 +48,36 @@ export async function updateApiConfigurationProto(
|
||||
|
||||
// Convert ModelInfo objects (empty arrays → undefined)
|
||||
// Plan Mode
|
||||
planModeOpenRouterModelInfo: protoApiConfiguration.planModeOpenRouterModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeOpenRouterModelInfo)
|
||||
: undefined,
|
||||
planModeOpenRouterModelInfo:
|
||||
protoApiConfiguration.planModeOpenRouterModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.planModeOpenRouterModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
planModeClineModelInfo: protoApiConfiguration.planModeClineModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeClineModelInfo)
|
||||
: undefined,
|
||||
planModeClinePassModelInfo:
|
||||
protoApiConfiguration.planModeClinePassModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.planModeClinePassModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
planModeOpenAiModelInfo: protoApiConfiguration.planModeOpenAiModelInfo
|
||||
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.planModeOpenAiModelInfo)
|
||||
: undefined,
|
||||
planModeHuggingFaceModelInfo: protoApiConfiguration.planModeHuggingFaceModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeHuggingFaceModelInfo)
|
||||
? fromProtobufOpenAiCompatibleModelInfo(
|
||||
protoApiConfiguration.planModeOpenAiModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
planModeHuggingFaceModelInfo:
|
||||
protoApiConfiguration.planModeHuggingFaceModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.planModeHuggingFaceModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
planModeLiteLlmModelInfo: protoApiConfiguration.planModeLiteLlmModelInfo
|
||||
? fromProtobufLiteLLMModelInfo(protoApiConfiguration.planModeLiteLlmModelInfo)
|
||||
? fromProtobufLiteLLMModelInfo(
|
||||
protoApiConfiguration.planModeLiteLlmModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
planModeRequestyModelInfo: protoApiConfiguration.planModeRequestyModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeRequestyModelInfo)
|
||||
@@ -65,34 +85,52 @@ export async function updateApiConfigurationProto(
|
||||
planModeGroqModelInfo: protoApiConfiguration.planModeGroqModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeGroqModelInfo)
|
||||
: undefined,
|
||||
planModeHuaweiCloudMaasModelInfo: protoApiConfiguration.planModeHuaweiCloudMaasModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeHuaweiCloudMaasModelInfo)
|
||||
: undefined,
|
||||
planModeHuaweiCloudMaasModelInfo:
|
||||
protoApiConfiguration.planModeHuaweiCloudMaasModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.planModeHuaweiCloudMaasModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
planModeBasetenModelInfo: protoApiConfiguration.planModeBasetenModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeBasetenModelInfo)
|
||||
: undefined,
|
||||
planModeVercelAiGatewayModelInfo: protoApiConfiguration.planModeVercelAiGatewayModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeVercelAiGatewayModelInfo)
|
||||
: undefined,
|
||||
planModeVercelAiGatewayModelInfo:
|
||||
protoApiConfiguration.planModeVercelAiGatewayModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.planModeVercelAiGatewayModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
planModeOcaModelInfo: protoApiConfiguration.planModeOcaModelInfo
|
||||
? fromProtobufOcaModelInfo(protoApiConfiguration.planModeOcaModelInfo)
|
||||
: undefined,
|
||||
planModeAihubmixModelInfo: protoApiConfiguration.planModeAihubmixModelInfo
|
||||
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.planModeAihubmixModelInfo)
|
||||
? fromProtobufOpenAiCompatibleModelInfo(
|
||||
protoApiConfiguration.planModeAihubmixModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
|
||||
// Act Mode
|
||||
actModeOpenRouterModelInfo: protoApiConfiguration.actModeOpenRouterModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeOpenRouterModelInfo)
|
||||
: undefined,
|
||||
actModeOpenRouterModelInfo:
|
||||
protoApiConfiguration.actModeOpenRouterModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.actModeOpenRouterModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
actModeClineModelInfo: protoApiConfiguration.actModeClineModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeClineModelInfo)
|
||||
: undefined,
|
||||
actModeClinePassModelInfo: protoApiConfiguration.actModeClinePassModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeClinePassModelInfo)
|
||||
: undefined,
|
||||
actModeOpenAiModelInfo: protoApiConfiguration.actModeOpenAiModelInfo
|
||||
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.actModeOpenAiModelInfo)
|
||||
? fromProtobufOpenAiCompatibleModelInfo(
|
||||
protoApiConfiguration.actModeOpenAiModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
actModeLiteLlmModelInfo: protoApiConfiguration.actModeLiteLlmModelInfo
|
||||
? fromProtobufLiteLLMModelInfo(protoApiConfiguration.actModeLiteLlmModelInfo)
|
||||
? fromProtobufLiteLLMModelInfo(
|
||||
protoApiConfiguration.actModeLiteLlmModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
actModeRequestyModelInfo: protoApiConfiguration.actModeRequestyModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeRequestyModelInfo)
|
||||
@@ -100,48 +138,67 @@ export async function updateApiConfigurationProto(
|
||||
actModeGroqModelInfo: protoApiConfiguration.actModeGroqModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeGroqModelInfo)
|
||||
: undefined,
|
||||
actModeHuggingFaceModelInfo: protoApiConfiguration.actModeHuggingFaceModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeHuggingFaceModelInfo)
|
||||
: undefined,
|
||||
actModeHuaweiCloudMaasModelInfo: protoApiConfiguration.actModeHuaweiCloudMaasModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeHuaweiCloudMaasModelInfo)
|
||||
: undefined,
|
||||
actModeHuggingFaceModelInfo:
|
||||
protoApiConfiguration.actModeHuggingFaceModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.actModeHuggingFaceModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
actModeHuaweiCloudMaasModelInfo:
|
||||
protoApiConfiguration.actModeHuaweiCloudMaasModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.actModeHuaweiCloudMaasModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
actModeBasetenModelInfo: protoApiConfiguration.actModeBasetenModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeBasetenModelInfo)
|
||||
: undefined,
|
||||
actModeVercelAiGatewayModelInfo: protoApiConfiguration.actModeVercelAiGatewayModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeVercelAiGatewayModelInfo)
|
||||
: undefined,
|
||||
actModeVercelAiGatewayModelInfo:
|
||||
protoApiConfiguration.actModeVercelAiGatewayModelInfo
|
||||
? fromProtobufModelInfo(
|
||||
protoApiConfiguration.actModeVercelAiGatewayModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
actModeOcaModelInfo: protoApiConfiguration.actModeOcaModelInfo
|
||||
? fromProtobufOcaModelInfo(protoApiConfiguration.actModeOcaModelInfo)
|
||||
: undefined,
|
||||
actModeAihubmixModelInfo: protoApiConfiguration.actModeAihubmixModelInfo
|
||||
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.actModeAihubmixModelInfo)
|
||||
? fromProtobufOpenAiCompatibleModelInfo(
|
||||
protoApiConfiguration.actModeAihubmixModelInfo,
|
||||
)
|
||||
: undefined,
|
||||
geminiPlanModeThinkingLevel: protoApiConfiguration.geminiPlanModeThinkingLevel,
|
||||
geminiActModeThinkingLevel: protoApiConfiguration.geminiActModeThinkingLevel,
|
||||
planModeReasoningEffort: protoApiConfiguration.planModeReasoningEffort as OpenaiReasoningEffort | undefined,
|
||||
actModeReasoningEffort: protoApiConfiguration.actModeReasoningEffort as OpenaiReasoningEffort | undefined,
|
||||
}
|
||||
geminiPlanModeThinkingLevel:
|
||||
protoApiConfiguration.geminiPlanModeThinkingLevel,
|
||||
geminiActModeThinkingLevel:
|
||||
protoApiConfiguration.geminiActModeThinkingLevel,
|
||||
planModeReasoningEffort: protoApiConfiguration.planModeReasoningEffort as
|
||||
| OpenaiReasoningEffort
|
||||
| undefined,
|
||||
actModeReasoningEffort: protoApiConfiguration.actModeReasoningEffort as
|
||||
| OpenaiReasoningEffort
|
||||
| undefined,
|
||||
};
|
||||
|
||||
// Update the API configuration in storage
|
||||
controller.stateManager.setApiConfiguration(convertedApiConfigurationFromProto)
|
||||
controller.stateManager.setApiConfiguration(
|
||||
convertedApiConfigurationFromProto,
|
||||
);
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode");
|
||||
controller.task.api = buildApiHandler(
|
||||
{ ...convertedApiConfigurationFromProto, ulid: controller.task.ulid },
|
||||
currentMode,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
await controller.postStateToWebview();
|
||||
|
||||
return Empty.create()
|
||||
return Empty.create();
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to update API configuration: ${error}`)
|
||||
throw error
|
||||
Logger.error(`Failed to update API configuration: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ 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 {
|
||||
@@ -2033,11 +2034,16 @@ export class Task {
|
||||
const model = this.api.getModel();
|
||||
const apiConfig = this.stateManager.getApiConfiguration();
|
||||
const mode = this.stateManager.getGlobalSettingsKey("mode");
|
||||
const providerId = (
|
||||
const configuredProviderId = (
|
||||
mode === "plan"
|
||||
? apiConfig.planModeApiProvider
|
||||
: apiConfig.actModeApiProvider
|
||||
) as string;
|
||||
const providerId =
|
||||
configuredProviderId === "cline-pass" &&
|
||||
!featureFlagsService.getBooleanFlagEnabled(FeatureFlag.CLINE_PASS)
|
||||
? "cline"
|
||||
: configuredProviderId;
|
||||
const customPrompt = this.stateManager.getGlobalSettingsKey("customPrompt");
|
||||
return { model, providerId, customPrompt, mode };
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ApiProvider =
|
||||
| "mistral"
|
||||
| "vscode-lm"
|
||||
| "cline"
|
||||
| "cline-pass"
|
||||
| "litellm"
|
||||
| "moonshot"
|
||||
| "nebius"
|
||||
@@ -1023,6 +1024,58 @@ export const clineDevstralModelInfo: ModelInfo = {
|
||||
description: "A stealth model for agentic coding tasks",
|
||||
}
|
||||
|
||||
export type ClinePassModelId = keyof typeof clinePassModels
|
||||
export const clinePassDefaultModelId = "cline-pass/glm-5.1"
|
||||
export const clinePassModelInfoSaneDefaults: ModelInfo = {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
description: "",
|
||||
}
|
||||
export const clinePassModels = {
|
||||
"cline-pass/glm-5.1": {
|
||||
name: "cline-pass/glm-5.1",
|
||||
maxTokens: 131_072,
|
||||
contextWindow: 202_752,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 0.98,
|
||||
outputPrice: 3.08,
|
||||
cacheReadsPrice: 0.182,
|
||||
cacheWritesPrice: 0,
|
||||
description: "",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
export function getModelSlug(modelId: string): string {
|
||||
return modelId.split("/").at(-1) ?? modelId
|
||||
}
|
||||
|
||||
export function buildModelInfoNameMap(models: Record<string, ModelInfo>): Record<string, ModelInfo> {
|
||||
const nameMap: Record<string, ModelInfo> = {}
|
||||
|
||||
for (const [id, info] of Object.entries(models)) {
|
||||
nameMap[getModelSlug(id)] = info
|
||||
}
|
||||
|
||||
return nameMap
|
||||
}
|
||||
|
||||
export function resolveClinePassModelInfo(modelId: string, modelInfoByName?: Record<string, ModelInfo>): ModelInfo {
|
||||
return (
|
||||
clinePassModels[modelId as keyof typeof clinePassModels] ??
|
||||
modelInfoByName?.[getModelSlug(modelId)] ??
|
||||
clinePassModelInfoSaneDefaults
|
||||
)
|
||||
}
|
||||
|
||||
export const OPENROUTER_PROVIDER_PREFERENCES: Record<string, { order: string[]; allow_fallbacks: boolean }> = {
|
||||
// Exacto Providers
|
||||
"moonshotai/kimi-k2:exacto": {
|
||||
|
||||
@@ -280,6 +280,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.VSCODE_LM
|
||||
case "cline":
|
||||
return ProtoApiProvider.CLINE
|
||||
case "cline-pass":
|
||||
return ProtoApiProvider.CLINE_PASS
|
||||
case "litellm":
|
||||
return ProtoApiProvider.LITELLM
|
||||
case "moonshot":
|
||||
@@ -372,6 +374,8 @@ export function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvid
|
||||
return "vscode-lm"
|
||||
case ProtoApiProvider.CLINE:
|
||||
return "cline"
|
||||
case ProtoApiProvider.CLINE_PASS:
|
||||
return "cline-pass"
|
||||
case ProtoApiProvider.LITELLM:
|
||||
return "litellm"
|
||||
case ProtoApiProvider.MOONSHOT:
|
||||
@@ -528,6 +532,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
planModeOpenRouterModelInfo: convertModelInfoToProtoOpenRouter(config.planModeOpenRouterModelInfo),
|
||||
planModeClineModelId: config.planModeClineModelId,
|
||||
planModeClineModelInfo: convertModelInfoToProtoOpenRouter(config.planModeClineModelInfo),
|
||||
planModeClinePassModelId: config.planModeClinePassModelId,
|
||||
planModeClinePassModelInfo: convertModelInfoToProtoOpenRouter(config.planModeClinePassModelInfo),
|
||||
planModeOpenAiModelId: config.planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo: convertOpenAiCompatibleModelInfoToProto(config.planModeOpenAiModelInfo),
|
||||
planModeOllamaModelId: config.planModeOllamaModelId,
|
||||
@@ -572,6 +578,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
actModeOpenRouterModelInfo: convertModelInfoToProtoOpenRouter(config.actModeOpenRouterModelInfo),
|
||||
actModeClineModelId: config.actModeClineModelId,
|
||||
actModeClineModelInfo: convertModelInfoToProtoOpenRouter(config.actModeClineModelInfo),
|
||||
actModeClinePassModelId: config.actModeClinePassModelId,
|
||||
actModeClinePassModelInfo: convertModelInfoToProtoOpenRouter(config.actModeClinePassModelInfo),
|
||||
actModeOpenAiModelId: config.actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo: convertOpenAiCompatibleModelInfoToProto(config.actModeOpenAiModelInfo),
|
||||
actModeOllamaModelId: config.actModeOllamaModelId,
|
||||
@@ -711,6 +719,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
planModeOpenRouterModelInfo: convertProtoToModelInfo(protoConfig.planModeOpenRouterModelInfo),
|
||||
planModeClineModelId: protoConfig.planModeClineModelId,
|
||||
planModeClineModelInfo: convertProtoToModelInfo(protoConfig.planModeClineModelInfo),
|
||||
planModeClinePassModelId: protoConfig.planModeClinePassModelId,
|
||||
planModeClinePassModelInfo: convertProtoToModelInfo(protoConfig.planModeClinePassModelInfo),
|
||||
planModeOpenAiModelId: protoConfig.planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.planModeOpenAiModelInfo),
|
||||
planModeOllamaModelId: protoConfig.planModeOllamaModelId,
|
||||
@@ -756,6 +766,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
actModeOpenRouterModelInfo: convertProtoToModelInfo(protoConfig.actModeOpenRouterModelInfo),
|
||||
actModeClineModelId: protoConfig.actModeClineModelId,
|
||||
actModeClineModelInfo: convertProtoToModelInfo(protoConfig.actModeClineModelInfo),
|
||||
actModeClinePassModelId: protoConfig.actModeClinePassModelId,
|
||||
actModeClinePassModelInfo: convertProtoToModelInfo(protoConfig.actModeClinePassModelInfo),
|
||||
actModeOpenAiModelId: protoConfig.actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.actModeOpenAiModelInfo),
|
||||
actModeOllamaModelId: protoConfig.actModeOllamaModelId,
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
"value": "cline",
|
||||
"label": "Cline"
|
||||
},
|
||||
{
|
||||
"value": "cline-pass",
|
||||
"label": "Cline Pass"
|
||||
},
|
||||
{
|
||||
"value": "openai-codex",
|
||||
"label": "ChatGPT Subscription"
|
||||
|
||||
@@ -15,6 +15,8 @@ export enum FeatureFlag {
|
||||
// Rollout flag for Cline provider model sourcing:
|
||||
// off => OpenRouter model list, on => Cline endpoint model list.
|
||||
EXTENSION_CLINE_MODELS_ENDPOINT = "extension_cline_models_endpoint",
|
||||
// Enables Cline Pass provider/model list exposure.
|
||||
CLINE_PASS = "ext-cline-pass",
|
||||
// Use the websocket mode for OpenAI native Responses API format
|
||||
OPENAI_RESPONSES_WEBSOCKET_MODE = "openai-responses-websocket-mode",
|
||||
}
|
||||
@@ -27,6 +29,7 @@ export const FeatureFlagDefaultValue: Partial<Record<FeatureFlag, FeatureFlagPay
|
||||
[FeatureFlag.EXTENSION_REMOTE_BANNERS_TTL]: 24 * 60 * 60 * 1000,
|
||||
[FeatureFlag.REMOTE_WELCOME_BANNERS]: process.env.E2E_TEST === "true" || process.env.IS_DEV === "true",
|
||||
[FeatureFlag.EXTENSION_CLINE_MODELS_ENDPOINT]: false,
|
||||
[FeatureFlag.CLINE_PASS]: false,
|
||||
[FeatureFlag.OPENAI_RESPONSES_WEBSOCKET_MODE]: false,
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,9 @@ describe("Provider key mapping", () => {
|
||||
expect(getProviderModelIdKey("cline", "act")).to.equal("actModeClineModelId")
|
||||
expect(getProviderModelIdKey("cline", "plan")).to.equal("planModeClineModelId")
|
||||
})
|
||||
|
||||
it("uses separate model keys for Cline Pass", () => {
|
||||
expect(getProviderModelIdKey("cline-pass", "act")).to.equal("actModeClinePassModelId")
|
||||
expect(getProviderModelIdKey("cline-pass", "plan")).to.equal("planModeClinePassModelId")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
anthropicDefaultModelId,
|
||||
basetenDefaultModelId,
|
||||
bedrockDefaultModelId,
|
||||
clinePassDefaultModelId,
|
||||
deepSeekDefaultModelId,
|
||||
fireworksDefaultModelId,
|
||||
geminiDefaultModelId,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
const ProviderKeyMap: Partial<Record<ApiProvider, string>> = {
|
||||
openrouter: "OpenRouterModelId",
|
||||
cline: "ClineModelId",
|
||||
"cline-pass": "ClinePassModelId",
|
||||
openai: "OpenAiModelId",
|
||||
ollama: "OllamaModelId",
|
||||
lmstudio: "LmStudioModelId",
|
||||
@@ -49,6 +51,7 @@ const ProviderKeyMap: Partial<Record<ApiProvider, string>> = {
|
||||
|
||||
export const ProviderToApiKeyMap: Partial<Record<ApiProvider, keyof Secrets | (keyof Secrets)[]>> = {
|
||||
cline: ["clineApiKey", "clineAccountId"],
|
||||
"cline-pass": ["clineApiKey", "clineAccountId"],
|
||||
anthropic: "apiKey",
|
||||
openrouter: "openRouterApiKey",
|
||||
bedrock: ["awsAccessKey", "awsBedrockApiKey"],
|
||||
@@ -91,6 +94,7 @@ const ProviderDefaultModelMap: Partial<Record<ApiProvider, string>> = {
|
||||
anthropic: anthropicDefaultModelId,
|
||||
openrouter: openRouterDefaultModelId,
|
||||
cline: openRouterDefaultModelId,
|
||||
"cline-pass": clinePassDefaultModelId,
|
||||
openai: openAiNativeDefaultModelId,
|
||||
ollama: "",
|
||||
lmstudio: "",
|
||||
|
||||
@@ -156,6 +156,8 @@ const API_HANDLER_SETTINGS_FIELDS = {
|
||||
planModeOpenRouterModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeClineModelId: { default: undefined as string | undefined },
|
||||
planModeClineModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeClinePassModelId: { default: undefined as string | undefined },
|
||||
planModeClinePassModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeOpenAiModelId: { default: undefined as string | undefined },
|
||||
planModeOpenAiModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
|
||||
planModeOllamaModelId: { default: undefined as string | undefined },
|
||||
@@ -200,6 +202,8 @@ const API_HANDLER_SETTINGS_FIELDS = {
|
||||
actModeOpenRouterModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeClineModelId: { default: undefined as string | undefined },
|
||||
actModeClineModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeClinePassModelId: { default: undefined as string | undefined },
|
||||
actModeClinePassModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeOpenAiModelId: { default: undefined as string | undefined },
|
||||
actModeOpenAiModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
|
||||
actModeOllamaModelId: { default: undefined as string | undefined },
|
||||
|
||||
@@ -10,6 +10,7 @@ export function isNextGenModelProvider(providerInfo: ApiProviderInfo): boolean {
|
||||
const providerId = normalize(providerInfo.providerId)
|
||||
return [
|
||||
"cline",
|
||||
"cline-pass",
|
||||
"anthropic",
|
||||
"bedrock",
|
||||
"gemini",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
|
||||
// Fall back to production when the authenticated user's app base URL is unavailable.
|
||||
const DEFAULT_APP_BASE_URL = "https://app.cline.bot"
|
||||
// Cline Pass subscribe/manage page in the dashboard.
|
||||
const CLINE_PASS_SUBSCRIBE_PATH = "/dashboard/plan"
|
||||
|
||||
/**
|
||||
* Optional subscribe affordance shown after a new user picks Cline Pass and
|
||||
* starts account creation. The account login itself is the existing OAuth flow;
|
||||
* this only surfaces a link to the dashboard subscribe page so the user can
|
||||
* activate Cline Pass. Purely additive — it does not change the onboarding flow.
|
||||
*/
|
||||
export const ClinePassSubscribeCallout = () => {
|
||||
const { clineUser } = useClineAuth()
|
||||
// Use the environment-aware app base URL (e.g. staging-app.cline.bot on staging)
|
||||
// so the subscribe link points at the same environment the user signs into.
|
||||
const appBaseUrl = clineUser?.appBaseUrl || DEFAULT_APP_BASE_URL
|
||||
const subscribeUrl = `${appBaseUrl}${CLINE_PASS_SUBSCRIBE_PATH}`
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-col gap-2 my-2 items-center">
|
||||
<p className="text-foreground/70 text-sm text-center m-0">
|
||||
Activate Cline Pass to unlock curated models — no API keys to manage.
|
||||
</p>
|
||||
<VSCodeButtonLink className="w-full" href={subscribeUrl}>
|
||||
<span className="codicon codicon-rocket mr-[6px] text-[14px]" />
|
||||
Get Cline Pass
|
||||
</VSCodeButtonLink>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import { buildModelInfoNameMap, type ModelInfo, resolveClinePassModelInfo } from "@shared/api"
|
||||
import type { OnboardingModel, OnboardingModelGroup, OpenRouterModelInfo } from "@shared/proto/index.cline"
|
||||
import { AlertCircleIcon, CircleCheckIcon, CircleIcon, ListIcon, LoaderCircleIcon, ZapIcon } from "lucide-react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
@@ -7,12 +7,15 @@ 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 ApiConfigurationSection from "../settings/sections/ApiConfigurationSection"
|
||||
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
|
||||
import WelcomeView from "../welcome/WelcomeView"
|
||||
import { ClinePassSubscribeCallout } from "./ClinePassSubscribeCallout"
|
||||
import {
|
||||
getCapabilities,
|
||||
getClineUIOnboardingGroups,
|
||||
@@ -20,11 +23,11 @@ import {
|
||||
getSpeedLabel,
|
||||
type OnboardingModelsByGroup,
|
||||
} from "./data-models"
|
||||
import { NEW_USER_TYPE, STEP_CONFIG, USER_TYPE_SELECTIONS } from "./data-steps"
|
||||
import { getUserTypeSelections, NEW_USER_TYPE, STEP_CONFIG } from "./data-steps"
|
||||
import { useOnboardingModels } from "./useOnboardingModels"
|
||||
|
||||
type ModelSelectionProps = {
|
||||
userType: NEW_USER_TYPE.FREE | NEW_USER_TYPE.POWER
|
||||
userType: NEW_USER_TYPE.FREE | NEW_USER_TYPE.POWER | NEW_USER_TYPE.CLINE_PASS
|
||||
selectedModelId: string
|
||||
onSelectModel: (modelId: string) => void
|
||||
onboardingModels: OnboardingModelsByGroup
|
||||
@@ -33,6 +36,13 @@ type ModelSelectionProps = {
|
||||
setSearchTerm: (term: string) => void
|
||||
}
|
||||
|
||||
function getModelGroupKey(userType: ModelSelectionProps["userType"]): keyof OnboardingModelsByGroup {
|
||||
if (userType === NEW_USER_TYPE.CLINE_PASS) {
|
||||
return "clinePass"
|
||||
}
|
||||
return userType === NEW_USER_TYPE.FREE ? "free" : "power"
|
||||
}
|
||||
|
||||
const ModelSelection = ({
|
||||
userType,
|
||||
selectedModelId,
|
||||
@@ -42,7 +52,9 @@ const ModelSelection = ({
|
||||
setSearchTerm,
|
||||
onboardingModels,
|
||||
}: ModelSelectionProps) => {
|
||||
const modelGroups = onboardingModels[userType === NEW_USER_TYPE.FREE ? "free" : "power"]
|
||||
const modelGroups = onboardingModels[getModelGroupKey(userType)]
|
||||
// Cline Pass costs are covered by the subscription, so price badges/ranges are not shown.
|
||||
const hidePrice = userType === NEW_USER_TYPE.CLINE_PASS
|
||||
|
||||
const searchedModels = useMemo(() => {
|
||||
if (!models || !searchTerm) {
|
||||
@@ -73,7 +85,7 @@ const ModelSelection = ({
|
||||
<Badge className="capitalize" variant="info">
|
||||
{model.badge}
|
||||
</Badge>
|
||||
) : model.info ? (
|
||||
) : !hidePrice && model.info ? (
|
||||
<Badge>{getPriceRange(model.info)}</Badge>
|
||||
) : null}
|
||||
</ItemTitle>
|
||||
@@ -99,7 +111,7 @@ const ModelSelection = ({
|
||||
<span>Context: </span>
|
||||
<span className="text-foreground/70">{(model?.info.contextWindow || 0) / 1000}k</span>
|
||||
</div>
|
||||
<Badge>{getPriceRange(model.info)}</Badge>
|
||||
{!hidePrice && <Badge>{getPriceRange(model.info)}</Badge>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -190,12 +202,13 @@ const ModelSelection = ({
|
||||
type UserTypeSelectionProps = {
|
||||
userType: NEW_USER_TYPE | undefined
|
||||
onSelectUserType: (type: NEW_USER_TYPE) => void
|
||||
userTypeSelections: ReturnType<typeof getUserTypeSelections>
|
||||
}
|
||||
|
||||
const UserTypeSelectionStep = ({ userType, onSelectUserType }: UserTypeSelectionProps) => (
|
||||
const UserTypeSelectionStep = ({ userType, onSelectUserType, userTypeSelections }: UserTypeSelectionProps) => (
|
||||
<div className="flex flex-col w-full items-center">
|
||||
<div className="flex w-full max-w-lg flex-col gap-3 my-2">
|
||||
{USER_TYPE_SELECTIONS.map((option) => {
|
||||
{userTypeSelections.map((option) => {
|
||||
const isSelected = userType === option.type
|
||||
|
||||
return (
|
||||
@@ -229,6 +242,7 @@ type OnboardingStepContentProps = {
|
||||
setSearchTerm: (term: string) => void
|
||||
models?: Record<string, ModelInfo>
|
||||
onboardingModels: OnboardingModelsByGroup
|
||||
userTypeSelections: ReturnType<typeof getUserTypeSelections>
|
||||
}
|
||||
|
||||
const OnboardingStepContent = ({
|
||||
@@ -241,14 +255,21 @@ const OnboardingStepContent = ({
|
||||
setSearchTerm,
|
||||
models,
|
||||
onboardingModels,
|
||||
userTypeSelections,
|
||||
}: OnboardingStepContentProps) => {
|
||||
if (step === 0) {
|
||||
return <UserTypeSelectionStep onSelectUserType={onSelectUserType} userType={userType} />
|
||||
return (
|
||||
<UserTypeSelectionStep
|
||||
onSelectUserType={onSelectUserType}
|
||||
userType={userType}
|
||||
userTypeSelections={userTypeSelections}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (step === 2) {
|
||||
return null
|
||||
}
|
||||
if (userType === NEW_USER_TYPE.FREE || userType === NEW_USER_TYPE.POWER) {
|
||||
if (userType === NEW_USER_TYPE.FREE || userType === NEW_USER_TYPE.POWER || userType === NEW_USER_TYPE.CLINE_PASS) {
|
||||
return (
|
||||
<ModelSelection
|
||||
models={models}
|
||||
@@ -268,6 +289,8 @@ 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 [stepNumber, setStepNumber] = useState(0)
|
||||
const [isActionLoading, setIsActionLoading] = useState(false)
|
||||
@@ -277,23 +300,29 @@ const OnboardingViewContent = ({ onboardingModels }: { onboardingModels: Onboard
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
|
||||
const models = useMemo(() => getClineUIOnboardingGroups(onboardingModels), [onboardingModels])
|
||||
// Cline Pass model IDs (e.g. "cline-pass/glm-5.1") are not keyed in openRouterModels,
|
||||
// so resolve their info via the slug-based lookup used by ClinePassProvider.
|
||||
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm("")
|
||||
const userGroup = userType === NEW_USER_TYPE.POWER ? NEW_USER_TYPE.POWER : NEW_USER_TYPE.FREE
|
||||
const modelGroup = models[userGroup][0]
|
||||
const userGroupInitModel = modelGroup.models[0]
|
||||
setSelectedModelId(userGroupInitModel.id)
|
||||
const groupKey = userType === NEW_USER_TYPE.CLINE_PASS ? "clinePass" : userType === NEW_USER_TYPE.POWER ? "power" : "free"
|
||||
// Some groups can be empty (e.g. Cline Pass list not returned yet); fall back to free.
|
||||
const modelGroup = models[groupKey][0] ?? models.free[0]
|
||||
const userGroupInitModel = modelGroup?.models[0]
|
||||
setSelectedModelId(userGroupInitModel?.id ?? "")
|
||||
}, [userType, models])
|
||||
|
||||
const onUserTypeClick = useCallback((userType: NEW_USER_TYPE) => {
|
||||
setUserType(userType)
|
||||
const action =
|
||||
userType === NEW_USER_TYPE.POWER
|
||||
? "power_user_selected"
|
||||
: userType === NEW_USER_TYPE.FREE
|
||||
? "free_user_selected"
|
||||
: "byok_user_selected"
|
||||
userType === NEW_USER_TYPE.CLINE_PASS
|
||||
? "cline_pass_user_selected"
|
||||
: userType === NEW_USER_TYPE.POWER
|
||||
? "power_user_selected"
|
||||
: userType === NEW_USER_TYPE.FREE
|
||||
? "free_user_selected"
|
||||
: "byok_user_selected"
|
||||
// User selection is available in step 0 only
|
||||
StateServiceClient.captureOnboardingProgress({ step: 0, action })
|
||||
}, [])
|
||||
@@ -308,21 +337,35 @@ const OnboardingViewContent = ({ onboardingModels }: { onboardingModels: Onboard
|
||||
async (updateModelId: boolean, step: number) => {
|
||||
const modelSelected = (updateModelId && selectedModelId) || undefined
|
||||
if (modelSelected) {
|
||||
await handleFieldsChange({
|
||||
planModeOpenRouterModelId: selectedModelId,
|
||||
actModeOpenRouterModelId: selectedModelId,
|
||||
planModeOpenRouterModelInfo: openRouterModels[selectedModelId],
|
||||
actModeOpenRouterModelInfo: openRouterModels[selectedModelId],
|
||||
planModeApiProvider: "cline",
|
||||
actModeApiProvider: "cline",
|
||||
})
|
||||
if (userType === NEW_USER_TYPE.CLINE_PASS) {
|
||||
// Cline Pass uses its own provider + model fields; costs are covered by the
|
||||
// subscription and it routes through the Cline endpoint.
|
||||
const clinePassModelInfo = resolveClinePassModelInfo(selectedModelId, openRouterModelsByName)
|
||||
await handleFieldsChange({
|
||||
planModeClinePassModelId: selectedModelId,
|
||||
actModeClinePassModelId: selectedModelId,
|
||||
planModeClinePassModelInfo: clinePassModelInfo,
|
||||
actModeClinePassModelInfo: clinePassModelInfo,
|
||||
planModeApiProvider: "cline-pass",
|
||||
actModeApiProvider: "cline-pass",
|
||||
})
|
||||
} else {
|
||||
await handleFieldsChange({
|
||||
planModeOpenRouterModelId: selectedModelId,
|
||||
actModeOpenRouterModelId: selectedModelId,
|
||||
planModeOpenRouterModelInfo: openRouterModels[selectedModelId],
|
||||
actModeOpenRouterModelInfo: openRouterModels[selectedModelId],
|
||||
planModeApiProvider: "cline",
|
||||
actModeApiProvider: "cline",
|
||||
})
|
||||
}
|
||||
}
|
||||
hideAccount()
|
||||
hideSettings()
|
||||
const action = "onboarding_completed"
|
||||
StateServiceClient.captureOnboardingProgress({ step, modelSelected, action, completed: true })
|
||||
},
|
||||
[hideAccount, hideSettings, handleFieldsChange, selectedModelId, openRouterModels],
|
||||
[hideAccount, hideSettings, handleFieldsChange, selectedModelId, openRouterModels, openRouterModelsByName, userType],
|
||||
)
|
||||
|
||||
const handleFooterAction = useCallback(
|
||||
@@ -377,6 +420,7 @@ const OnboardingViewContent = ({ onboardingModels }: { onboardingModels: Onboard
|
||||
{stepNumber === 2 && (
|
||||
<div className="flex w-full max-w-lg flex-col gap-6 my-4 items-center ">
|
||||
<LoaderCircleIcon className="animate-spin" />
|
||||
{userType === NEW_USER_TYPE.CLINE_PASS && <ClinePassSubscribeCallout />}
|
||||
</div>
|
||||
)}
|
||||
{stepDisplayInfo.description && (
|
||||
@@ -394,6 +438,7 @@ const OnboardingViewContent = ({ onboardingModels }: { onboardingModels: Onboard
|
||||
setSearchTerm={setSearchTerm}
|
||||
step={stepNumber}
|
||||
userType={userType}
|
||||
userTypeSelections={userTypeSelections}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { OnboardingModel, OnboardingModelGroup } from "@shared/proto/cline/state"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getClineUIOnboardingGroups } from "../data-models"
|
||||
|
||||
function model(id: string, group: string): OnboardingModel {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
group,
|
||||
badge: "",
|
||||
score: 0,
|
||||
latency: 0,
|
||||
info: undefined,
|
||||
} as OnboardingModel
|
||||
}
|
||||
|
||||
function groupOf(models: OnboardingModel[]): OnboardingModelGroup {
|
||||
return { models } as OnboardingModelGroup
|
||||
}
|
||||
|
||||
describe("getClineUIOnboardingGroups", () => {
|
||||
it("buckets Cline Pass models into the clinePass group", () => {
|
||||
const result = getClineUIOnboardingGroups(
|
||||
groupOf([
|
||||
model("cline-pass/glm-5.1", "cline pass"),
|
||||
model("free-model", "free"),
|
||||
model("anthropic/claude", "frontier"),
|
||||
model("z-ai/glm", "open source"),
|
||||
]),
|
||||
)
|
||||
|
||||
expect(result.clinePass).toHaveLength(1)
|
||||
expect(result.clinePass[0].group).toBe("cline pass")
|
||||
expect(result.clinePass[0].models.map((m) => m.id)).toEqual(["cline-pass/glm-5.1"])
|
||||
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"])
|
||||
})
|
||||
|
||||
it("returns an empty clinePass group when no Cline Pass models are present", () => {
|
||||
const result = getClineUIOnboardingGroups(groupOf([model("free-model", "free")]))
|
||||
expect(result.clinePass).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getUserTypeSelections, NEW_USER_TYPE } from "../data-steps"
|
||||
|
||||
describe("getUserTypeSelections", () => {
|
||||
it("omits the Cline Pass option when the flag is disabled", () => {
|
||||
const selections = getUserTypeSelections(false)
|
||||
expect(selections.map((s) => s.type)).toEqual([NEW_USER_TYPE.FREE, NEW_USER_TYPE.POWER, NEW_USER_TYPE.BYOK])
|
||||
expect(selections.some((s) => s.type === NEW_USER_TYPE.CLINE_PASS)).toBe(false)
|
||||
})
|
||||
|
||||
it("surfaces Cline Pass first when the flag is enabled", () => {
|
||||
const selections = getUserTypeSelections(true)
|
||||
expect(selections[0]?.type).toBe(NEW_USER_TYPE.CLINE_PASS)
|
||||
expect(selections.map((s) => s.type)).toEqual([
|
||||
NEW_USER_TYPE.CLINE_PASS,
|
||||
NEW_USER_TYPE.FREE,
|
||||
NEW_USER_TYPE.POWER,
|
||||
NEW_USER_TYPE.BYOK,
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import type { OpenRouterModelInfo } from "@shared/proto/cline/models"
|
||||
import type { OnboardingModel, OnboardingModelGroup } from "@shared/proto/cline/state"
|
||||
|
||||
export interface OnboardingModelsByGroup {
|
||||
clinePass: ModelGroup[]
|
||||
free: ModelGroup[]
|
||||
power: ModelGroup[]
|
||||
}
|
||||
@@ -14,11 +15,13 @@ interface ModelGroup {
|
||||
export function getClineUIOnboardingGroups(groupedModels: OnboardingModelGroup): OnboardingModelsByGroup {
|
||||
const { models } = groupedModels
|
||||
|
||||
const clinePassModels = models.filter((m) => m.group === "cline pass")
|
||||
const freeModels = models.filter((m) => m.group === "free")
|
||||
const frontierModels = models.filter((m) => m.group === "frontier")
|
||||
const openSourceModels = models.filter((m) => m.group === "open source")
|
||||
|
||||
return {
|
||||
clinePass: clinePassModels.length > 0 ? [{ group: "cline pass", models: clinePassModels }] : [],
|
||||
free: freeModels.length > 0 ? [{ group: "free", models: freeModels }] : [],
|
||||
power: [
|
||||
...(frontierModels.length > 0 ? [{ group: "frontier", models: frontierModels }] : []),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum NEW_USER_TYPE {
|
||||
CLINE_PASS = "cline-pass",
|
||||
FREE = "free",
|
||||
POWER = "power",
|
||||
BYOK = "byok",
|
||||
@@ -19,6 +20,13 @@ export const STEP_CONFIG = {
|
||||
{ text: "Login to Cline", action: "signin", variant: "secondary" },
|
||||
],
|
||||
},
|
||||
[NEW_USER_TYPE.CLINE_PASS]: {
|
||||
title: "Select a Cline Pass model",
|
||||
buttons: [
|
||||
{ text: "Create my Account", action: "signup", variant: "default" },
|
||||
{ text: "Back", action: "back", variant: "secondary" },
|
||||
],
|
||||
},
|
||||
[NEW_USER_TYPE.FREE]: {
|
||||
title: "Select a free model",
|
||||
buttons: [
|
||||
@@ -47,8 +55,24 @@ export const STEP_CONFIG = {
|
||||
},
|
||||
} as const
|
||||
|
||||
export const USER_TYPE_SELECTIONS: UserTypeSelection[] = [
|
||||
const CLINE_PASS_USER_TYPE_SELECTION: UserTypeSelection = {
|
||||
title: "Cline Pass",
|
||||
description: "Recommended — curated models, one subscription, no API keys to manage",
|
||||
type: NEW_USER_TYPE.CLINE_PASS,
|
||||
}
|
||||
|
||||
const BASE_USER_TYPE_SELECTIONS: UserTypeSelection[] = [
|
||||
{ title: "Absolutely Free", description: "Get started at no cost", type: NEW_USER_TYPE.FREE },
|
||||
{ title: "Frontier Model", description: "Claude, GPT Codex, Gemini, etc.", type: NEW_USER_TYPE.POWER },
|
||||
{ title: "Bring my own API key", description: "Use Cline with your provider of choice", type: NEW_USER_TYPE.BYOK },
|
||||
]
|
||||
|
||||
/**
|
||||
* Returns the onboarding user-type options. Cline Pass is surfaced as a
|
||||
* recommended-but-optional choice at the top of the list only when the
|
||||
* `ext-cline-pass` feature flag is enabled; otherwise it is omitted entirely
|
||||
* and the classic Free / Frontier / BYOK options are shown.
|
||||
*/
|
||||
export function getUserTypeSelections(isClinePassEnabled: boolean): UserTypeSelection[] {
|
||||
return isClinePassEnabled ? [CLINE_PASS_USER_TYPE_SELECTION, ...BASE_USER_TYPE_SELECTIONS] : BASE_USER_TYPE_SELECTIONS
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import { buildModelInfoNameMap, type ModelInfo, resolveClinePassModelInfo } from "@shared/api"
|
||||
import { CLINE_ONBOARDING_MODELS } from "@shared/cline/onboarding"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import type { ClineRecommendedModel } from "@shared/proto/cline/models"
|
||||
@@ -47,6 +47,7 @@ function toOnboardingModel(
|
||||
interface RecommendedModelsData {
|
||||
recommended: ClineRecommendedModel[]
|
||||
free: ClineRecommendedModel[]
|
||||
clinePass: ClineRecommendedModel[]
|
||||
}
|
||||
|
||||
type FetchState = { status: "loading" } | { status: "success"; data: RecommendedModelsData } | { status: "empty" }
|
||||
@@ -64,10 +65,11 @@ export function useOnboardingModels(): UseOnboardingModelsResult {
|
||||
if (!cancelled) {
|
||||
const recommended = response.recommended ?? []
|
||||
const free = response.free ?? []
|
||||
if (recommended.length === 0 && free.length === 0) {
|
||||
const clinePass = response.clinePass ?? []
|
||||
if (recommended.length === 0 && free.length === 0 && clinePass.length === 0) {
|
||||
setFetchState({ status: "empty" })
|
||||
} else {
|
||||
setFetchState({ status: "success", data: { recommended, free } })
|
||||
setFetchState({ status: "success", data: { recommended, free, clinePass } })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -93,6 +95,11 @@ export function useOnboardingModels(): UseOnboardingModelsResult {
|
||||
return { ...openRouterModels, ...(clineModels ?? {}) }
|
||||
}, [openRouterModels, clineModels])
|
||||
|
||||
// Cline Pass model IDs omit the upstream lab (e.g. "cline-pass/glm-5.1"), so look up
|
||||
// capabilities via the model slug against the OpenRouter catalog, falling back to
|
||||
// conservative Cline Pass defaults. Mirrors ClinePassProvider's resolution.
|
||||
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
|
||||
|
||||
return useMemo<UseOnboardingModelsResult>(() => {
|
||||
if (fetchState.status !== "success") {
|
||||
return { status: fetchState.status, models: { models: CLINE_ONBOARDING_MODELS } }
|
||||
@@ -101,7 +108,11 @@ export function useOnboardingModels(): UseOnboardingModelsResult {
|
||||
const { data } = fetchState
|
||||
const freeModels = data.free.map((rec) => toOnboardingModel(rec, "free", "Free", modelCatalog))
|
||||
const frontierModels = data.recommended.map((rec) => toOnboardingModel(rec, "frontier", "", modelCatalog))
|
||||
const clinePassCatalog = Object.fromEntries(
|
||||
data.clinePass.map((rec) => [rec.id, resolveClinePassModelInfo(rec.id, openRouterModelsByName)]),
|
||||
)
|
||||
const clinePassModels = data.clinePass.map((rec) => toOnboardingModel(rec, "cline pass", "", clinePassCatalog))
|
||||
|
||||
return { status: "success", models: { models: [...freeModels, ...frontierModels] } }
|
||||
}, [fetchState, modelCatalog])
|
||||
return { status: "success", models: { models: [...clinePassModels, ...freeModels, ...frontierModels] } }
|
||||
}, [fetchState, modelCatalog, openRouterModelsByName])
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ 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"
|
||||
@@ -19,6 +21,7 @@ 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"
|
||||
@@ -99,8 +102,9 @@ 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)
|
||||
const { selectedProvider } = normalizeApiConfiguration(apiConfiguration, currentMode, { isClinePassEnabled })
|
||||
|
||||
const { handleModeFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
@@ -141,6 +145,9 @@ 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
|
||||
@@ -154,7 +161,7 @@ const ApiOptions = ({
|
||||
}
|
||||
|
||||
return providers
|
||||
}, [remoteConfigSettings])
|
||||
}, [isClinePassEnabled, remoteConfigSettings])
|
||||
|
||||
const currentProviderLabel = useMemo(() => {
|
||||
return providerOptions.find((option) => option.value === selectedProvider)?.label || selectedProvider
|
||||
@@ -363,11 +370,16 @@ const ApiOptions = ({
|
||||
<ClineProvider
|
||||
currentMode={currentMode}
|
||||
initialModelTab={initialModelTab}
|
||||
isClinePassEnabled={isClinePassEnabled}
|
||||
isPopup={isPopup}
|
||||
showModelOptions={showModelOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{apiConfiguration && isClinePassEnabled && selectedProvider === "cline-pass" && (
|
||||
<ClinePassProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
|
||||
)}
|
||||
|
||||
{apiConfiguration && selectedProvider === "asksage" && (
|
||||
<AskSageProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CLAUDE_SONNET_1M_SUFFIX, openRouterDefaultModelId } from "@shared/api"
|
||||
import { type ApiConfiguration, buildModelInfoNameMap, CLAUDE_SONNET_1M_SUFFIX, type ModelInfo } from "@shared/api"
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@shared/cline/recommended-models"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import { type ClineRecommendedModel, ClineRecommendedModelsResponse } from "@shared/proto/cline/models"
|
||||
@@ -52,6 +52,12 @@ export interface ClineModelPickerProps {
|
||||
currentMode: Mode
|
||||
showProviderRouting?: boolean
|
||||
initialTab?: "recommended" | "free"
|
||||
defaultModelId?: string
|
||||
modelIdFieldPair?: { plan: keyof ApiConfiguration; act: keyof ApiConfiguration }
|
||||
modelInfoFieldPair?: { plan: keyof ApiConfiguration; act: keyof ApiConfiguration }
|
||||
models?: Record<string, ModelInfo>
|
||||
isClinePassEnabled?: boolean
|
||||
showFeaturedModels?: boolean
|
||||
}
|
||||
|
||||
interface FeaturedModelCardEntry {
|
||||
@@ -92,11 +98,44 @@ const FREE_MODELS_FALLBACK: FeaturedModelCardEntry[] = CLINE_RECOMMENDED_MODELS_
|
||||
.map((model) => toFeaturedModelCardEntry(model, "FREE"))
|
||||
.filter((model): model is FeaturedModelCardEntry => model !== null)
|
||||
|
||||
const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMode, showProviderRouting, initialTab }) => {
|
||||
const ClineModelPicker: React.FC<ClineModelPickerProps> = ({
|
||||
isPopup,
|
||||
currentMode,
|
||||
showProviderRouting,
|
||||
initialTab,
|
||||
defaultModelId,
|
||||
modelIdFieldPair = { plan: "planModeClineModelId", act: "actModeClineModelId" },
|
||||
modelInfoFieldPair = { plan: "planModeClineModelInfo", act: "actModeClineModelInfo" },
|
||||
models,
|
||||
isClinePassEnabled = true,
|
||||
showFeaturedModels = true,
|
||||
}) => {
|
||||
const { handleModeFieldsChange, handleFieldChange } = useApiConfigurationHandlers()
|
||||
const { apiConfiguration, favoritedModelIds, clineModels, refreshClineModels } = useExtensionState()
|
||||
const { apiConfiguration, favoritedModelIds, clineModels, openRouterModels, refreshClineModels } = useExtensionState()
|
||||
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
const [searchTerm, setSearchTerm] = useState(modeFields.clineModelId || openRouterDefaultModelId)
|
||||
const resolvedModels = models ?? clineModels
|
||||
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
|
||||
const normalizedSelection = useMemo(
|
||||
() =>
|
||||
normalizeApiConfiguration(apiConfiguration, currentMode, {
|
||||
isClinePassEnabled,
|
||||
clinePassModelInfoByName: openRouterModelsByName,
|
||||
}),
|
||||
[apiConfiguration, currentMode, isClinePassEnabled, openRouterModelsByName],
|
||||
)
|
||||
const configuredModelId = apiConfiguration?.[modelIdFieldPair[currentMode]] as string | undefined
|
||||
const selectedOrDefaultModelId = defaultModelId ?? normalizedSelection.selectedModelId
|
||||
const resolveModelId = useCallback(
|
||||
(modelId?: string) => {
|
||||
if (models && (!modelId || !(modelId in models))) {
|
||||
return selectedOrDefaultModelId
|
||||
}
|
||||
|
||||
return modelId || selectedOrDefaultModelId
|
||||
},
|
||||
[models, selectedOrDefaultModelId],
|
||||
)
|
||||
const [searchTerm, setSearchTerm] = useState(resolveModelId(configuredModelId))
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const [clineRecommendedModels, setClineRecommendedModels] = useState<FeaturedModelCardEntry[]>([])
|
||||
@@ -188,9 +227,9 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
if (initialTab) {
|
||||
return
|
||||
}
|
||||
const currentModelId = modeFields.clineModelId || openRouterDefaultModelId
|
||||
const currentModelId = resolveModelId(configuredModelId)
|
||||
setActiveTab(freeClineModelIdSet.has(normalizeModelId(currentModelId)) ? "free" : "recommended")
|
||||
}, [modeFields.clineModelId, freeClineModelIdSet, initialTab])
|
||||
}, [configuredModelId, freeClineModelIdSet, initialTab, resolveModelId])
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
@@ -200,19 +239,27 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
|
||||
handleModeFieldsChange(
|
||||
{
|
||||
clineModelId: { plan: "planModeClineModelId", act: "actModeClineModelId" },
|
||||
clineModelInfo: { plan: "planModeClineModelInfo", act: "actModeClineModelInfo" },
|
||||
clineModelId: modelIdFieldPair,
|
||||
clineModelInfo: modelInfoFieldPair,
|
||||
},
|
||||
{
|
||||
clineModelId: newModelId,
|
||||
clineModelInfo: clineModels?.[newModelId],
|
||||
clineModelInfo: resolvedModels?.[newModelId],
|
||||
},
|
||||
currentMode,
|
||||
)
|
||||
}
|
||||
|
||||
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
const selected = normalizeApiConfiguration(apiConfiguration, currentMode)
|
||||
const resolvedModelId = resolveModelId(configuredModelId)
|
||||
const selected =
|
||||
(defaultModelId || models) && resolvedModelId !== normalizedSelection.selectedModelId
|
||||
? {
|
||||
...normalizedSelection,
|
||||
selectedModelId: resolvedModelId,
|
||||
selectedModelInfo: resolvedModels?.[resolvedModelId] ?? normalizedSelection.selectedModelInfo,
|
||||
}
|
||||
: normalizedSelection
|
||||
if (freeClineModelIdSet.has(normalizeModelId(selected.selectedModelId))) {
|
||||
return {
|
||||
...selected,
|
||||
@@ -226,10 +273,12 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}, [apiConfiguration, currentMode, freeClineModelIdSet])
|
||||
}, [configuredModelId, defaultModelId, freeClineModelIdSet, models, normalizedSelection, resolvedModels, resolveModelId])
|
||||
|
||||
useMount(() => {
|
||||
refreshClineModels()
|
||||
if (!models) {
|
||||
refreshClineModels()
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -238,9 +287,8 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
|
||||
// Sync external changes when the modelId changes
|
||||
useEffect(() => {
|
||||
const currentModelId = modeFields.clineModelId || openRouterDefaultModelId
|
||||
setSearchTerm(currentModelId)
|
||||
}, [modeFields.clineModelId])
|
||||
setSearchTerm(resolveModelId(configuredModelId))
|
||||
}, [configuredModelId, resolveModelId])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -256,9 +304,9 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
}, [])
|
||||
|
||||
const modelIds = useMemo(() => {
|
||||
const unfilteredModelIds = Object.keys(clineModels ?? {}).sort((a, b) => a.localeCompare(b))
|
||||
const unfilteredModelIds = Object.keys(resolvedModels ?? {}).sort((a, b) => a.localeCompare(b))
|
||||
return filterOpenRouterModelIds(unfilteredModelIds, "cline", freeClineModelIds)
|
||||
}, [clineModels, freeClineModelIds])
|
||||
}, [resolvedModels, freeClineModelIds])
|
||||
|
||||
const searchableItems = useMemo(() => {
|
||||
return modelIds.map((id) => ({
|
||||
@@ -363,7 +411,7 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
return false
|
||||
}
|
||||
return (
|
||||
Object.entries(clineModels ?? {})?.some(([id, m]) => id === selectedModelId && m.thinkingConfig) ||
|
||||
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-4.6") ||
|
||||
@@ -377,7 +425,7 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
selectedModelIdLower.includes("claude-3.7-sonnet") ||
|
||||
selectedModelIdLower.includes("claude-3.7-sonnet:thinking")
|
||||
)
|
||||
}, [clineModels, selectedModelId, selectedModelIdLower, showReasoningEffort])
|
||||
}, [resolvedModels, selectedModelId, selectedModelIdLower, showReasoningEffort])
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", paddingBottom: 2 }}>
|
||||
@@ -394,49 +442,51 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
|
||||
<>
|
||||
{/* Tabs */}
|
||||
<TabsContainer style={{ marginTop: 4 }}>
|
||||
<Tab active={activeTab === "recommended"} onClick={() => setActiveTab("recommended")}>
|
||||
Recommended
|
||||
</Tab>
|
||||
<Tab active={activeTab === "free"} onClick={() => setActiveTab("free")}>
|
||||
Free
|
||||
</Tab>
|
||||
</TabsContainer>
|
||||
{showFeaturedModels && (
|
||||
<>
|
||||
{/* Tabs */}
|
||||
<TabsContainer style={{ marginTop: 4 }}>
|
||||
<Tab active={activeTab === "recommended"} onClick={() => setActiveTab("recommended")}>
|
||||
Recommended
|
||||
</Tab>
|
||||
<Tab active={activeTab === "free"} onClick={() => setActiveTab("free")}>
|
||||
Free
|
||||
</Tab>
|
||||
</TabsContainer>
|
||||
|
||||
{/* Model Cards */}
|
||||
<div style={{ marginBottom: "6px" }}>
|
||||
{activeTab === "recommended" &&
|
||||
recommendedModels.map((model) => (
|
||||
<FeaturedModelCard
|
||||
description={model.description}
|
||||
isSelected={selectedModelId === model.id}
|
||||
key={model.id}
|
||||
label={model.label}
|
||||
modelId={model.id}
|
||||
onClick={() => {
|
||||
handleModelChange(model.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{activeTab === "free" &&
|
||||
freeModels.map((model) => (
|
||||
<FeaturedModelCard
|
||||
description={model.description}
|
||||
isSelected={selectedModelId === model.id}
|
||||
key={model.id}
|
||||
label={model.label}
|
||||
modelId={model.id}
|
||||
onClick={() => {
|
||||
handleModelChange(model.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
{/* Model Cards */}
|
||||
<div style={{ marginBottom: "6px" }}>
|
||||
{activeTab === "recommended" &&
|
||||
recommendedModels.map((model) => (
|
||||
<FeaturedModelCard
|
||||
description={model.description}
|
||||
isSelected={selectedModelId === model.id}
|
||||
key={model.id}
|
||||
label={model.label}
|
||||
modelId={model.id}
|
||||
onClick={() => {
|
||||
handleModelChange(model.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{activeTab === "free" &&
|
||||
freeModels.map((model) => (
|
||||
<FeaturedModelCard
|
||||
description={model.description}
|
||||
isSelected={selectedModelId === model.id}
|
||||
key={model.id}
|
||||
label={model.label}
|
||||
modelId={model.id}
|
||||
onClick={() => {
|
||||
handleModelChange(model.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownWrapper ref={dropdownRef}>
|
||||
<VSCodeTextField
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
buildModelInfoNameMap,
|
||||
clinePassDefaultModelId,
|
||||
clinePassModels,
|
||||
type ModelInfo,
|
||||
resolveClinePassModelInfo,
|
||||
} from "@shared/api"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import ClineModelPicker from "../ClineModelPicker"
|
||||
import { ClineProvider } from "./ClineProvider"
|
||||
|
||||
export const ClinePassProvider: typeof ClineProvider = (props) => {
|
||||
const { openRouterModels } = useExtensionState()
|
||||
const openRouterModelsByName = useMemo(() => buildModelInfoNameMap(openRouterModels), [openRouterModels])
|
||||
const [clinePassRecommendedModels, setClinePassRecommendedModels] = useState<Record<string, ModelInfo> | undefined>(undefined)
|
||||
|
||||
const refreshClinePassModels = useCallback(async () => {
|
||||
try {
|
||||
const response = await ModelsServiceClient.refreshClineRecommendedModelsRpc(EmptyRequest.create({}))
|
||||
const models = Object.fromEntries(
|
||||
(response.clinePass ?? [])
|
||||
.filter((model) => model.id)
|
||||
.map((model) => {
|
||||
// Cline Pass 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,
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
setClinePassRecommendedModels(Object.keys(models).length > 0 ? models : undefined)
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh Cline Pass models:", error)
|
||||
}
|
||||
}, [openRouterModelsByName])
|
||||
|
||||
useEffect(() => {
|
||||
void refreshClinePassModels()
|
||||
}, [refreshClinePassModels])
|
||||
|
||||
const clinePassModelOptions = clinePassRecommendedModels ?? clinePassModels
|
||||
const clinePassDefaultModel = useMemo(() => {
|
||||
if (!clinePassModelOptions) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return clinePassModelOptions[clinePassDefaultModelId]
|
||||
? clinePassDefaultModelId
|
||||
: (Object.keys(clinePassModelOptions)[0] ?? clinePassDefaultModelId)
|
||||
}, [clinePassModelOptions])
|
||||
|
||||
return (
|
||||
<ClineModelPicker
|
||||
{...props}
|
||||
defaultModelId={clinePassDefaultModel}
|
||||
modelIdFieldPair={{ plan: "planModeClinePassModelId", act: "actModeClinePassModelId" }}
|
||||
modelInfoFieldPair={{ plan: "planModeClinePassModelInfo", act: "actModeClinePassModelInfo" }}
|
||||
models={clinePassModelOptions}
|
||||
showFeaturedModels={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -10,12 +10,19 @@ interface ClineProviderProps {
|
||||
isPopup?: boolean
|
||||
currentMode: Mode
|
||||
initialModelTab?: "recommended" | "free"
|
||||
isClinePassEnabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cline provider configuration component
|
||||
*/
|
||||
export const ClineProvider = ({ showModelOptions, isPopup, currentMode, initialModelTab }: ClineProviderProps) => {
|
||||
export const ClineProvider = ({
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
currentMode,
|
||||
initialModelTab,
|
||||
isClinePassEnabled,
|
||||
}: ClineProviderProps) => {
|
||||
return (
|
||||
<div>
|
||||
{/* Cline Account Info Card */}
|
||||
@@ -24,14 +31,13 @@ export const ClineProvider = ({ showModelOptions, isPopup, currentMode, initialM
|
||||
</div>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ClineModelPicker
|
||||
currentMode={currentMode}
|
||||
initialTab={initialModelTab}
|
||||
isPopup={isPopup}
|
||||
showProviderRouting={true}
|
||||
/>
|
||||
</>
|
||||
<ClineModelPicker
|
||||
currentMode={currentMode}
|
||||
initialTab={initialModelTab}
|
||||
isClinePassEnabled={isClinePassEnabled}
|
||||
isPopup={isPopup}
|
||||
showProviderRouting={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
cerebrasModels,
|
||||
claudeCodeDefaultModelId,
|
||||
claudeCodeModels,
|
||||
clinePassDefaultModelId,
|
||||
clinePassModels,
|
||||
deepSeekDefaultModelId,
|
||||
deepSeekModels,
|
||||
doubaoDefaultModelId,
|
||||
@@ -59,6 +61,7 @@ import {
|
||||
qwenCodeModels,
|
||||
requestyDefaultModelId,
|
||||
requestyDefaultModelInfo,
|
||||
resolveClinePassModelInfo,
|
||||
sambanovaDefaultModelId,
|
||||
sambanovaModels,
|
||||
sapAiCoreDefaultModelId,
|
||||
@@ -102,6 +105,8 @@ export function getModelsForProvider(
|
||||
return openAiNativeModels
|
||||
case "openai-codex":
|
||||
return openAiCodexModels
|
||||
case "cline-pass":
|
||||
return clinePassModels
|
||||
case "deepseek":
|
||||
return deepSeekModels
|
||||
case "qwen":
|
||||
@@ -180,9 +185,11 @@ export interface NormalizedApiConfig {
|
||||
export function normalizeApiConfiguration(
|
||||
apiConfiguration: ApiConfiguration | undefined,
|
||||
currentMode: Mode,
|
||||
options: { isClinePassEnabled?: boolean; clinePassModelInfoByName?: Record<string, ModelInfo> } = {},
|
||||
): NormalizedApiConfig {
|
||||
const provider =
|
||||
const configuredProvider =
|
||||
(currentMode === "plan" ? apiConfiguration?.planModeApiProvider : apiConfiguration?.actModeApiProvider) || "anthropic"
|
||||
const provider = configuredProvider === "cline-pass" && options.isClinePassEnabled === false ? "cline" : configuredProvider
|
||||
|
||||
const modelId = currentMode === "plan" ? apiConfiguration?.planModeApiModelId : apiConfiguration?.actModeApiModelId
|
||||
|
||||
@@ -280,10 +287,9 @@ export function normalizeApiConfiguration(
|
||||
currentMode === "plan"
|
||||
? apiConfiguration?.planModeOpenRouterModelInfo
|
||||
: apiConfiguration?.actModeOpenRouterModelInfo
|
||||
const clineModelId =
|
||||
(currentMode === "plan" ? apiConfiguration?.planModeClineModelId : apiConfiguration?.actModeClineModelId) ||
|
||||
fallbackOpenRouterModelId ||
|
||||
openRouterDefaultModelId
|
||||
const configuredClineModelId =
|
||||
currentMode === "plan" ? apiConfiguration?.planModeClineModelId : apiConfiguration?.actModeClineModelId
|
||||
const clineModelId = configuredClineModelId || fallbackOpenRouterModelId || openRouterDefaultModelId
|
||||
const clineModelInfo =
|
||||
(currentMode === "plan" ? apiConfiguration?.planModeClineModelInfo : apiConfiguration?.actModeClineModelInfo) ||
|
||||
fallbackOpenRouterModelInfo ||
|
||||
@@ -293,6 +299,22 @@ export function normalizeApiConfiguration(
|
||||
selectedModelId: clineModelId,
|
||||
selectedModelInfo: clineModelInfo,
|
||||
}
|
||||
case "cline-pass":
|
||||
const configuredClinePassModelId =
|
||||
currentMode === "plan" ? apiConfiguration?.planModeClinePassModelId : apiConfiguration?.actModeClinePassModelId
|
||||
const clinePassModelId = configuredClinePassModelId?.startsWith("cline-pass/")
|
||||
? configuredClinePassModelId
|
||||
: clinePassDefaultModelId
|
||||
const clinePassModelInfo =
|
||||
(currentMode === "plan"
|
||||
? apiConfiguration?.planModeClinePassModelInfo
|
||||
: apiConfiguration?.actModeClinePassModelInfo) ||
|
||||
resolveClinePassModelInfo(clinePassModelId, options.clinePassModelInfoByName)
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: clinePassModelId,
|
||||
selectedModelInfo: clinePassModelInfo,
|
||||
}
|
||||
case "openai":
|
||||
const openAiModelId =
|
||||
currentMode === "plan" ? apiConfiguration?.planModeOpenAiModelId : apiConfiguration?.actModeOpenAiModelId
|
||||
@@ -532,6 +554,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
openAiModelId: undefined,
|
||||
openRouterModelId: undefined,
|
||||
clineModelId: undefined,
|
||||
clinePassModelId: undefined,
|
||||
groqModelId: undefined,
|
||||
basetenModelId: undefined,
|
||||
huggingFaceModelId: undefined,
|
||||
@@ -546,6 +569,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
liteLlmModelInfo: undefined,
|
||||
openRouterModelInfo: undefined,
|
||||
clineModelInfo: undefined,
|
||||
clinePassModelInfo: undefined,
|
||||
requestyModelInfo: undefined,
|
||||
groqModelInfo: undefined,
|
||||
basetenModelInfo: undefined,
|
||||
@@ -577,6 +601,10 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
const clineModelInfo =
|
||||
(mode === "plan" ? apiConfiguration.planModeClineModelInfo : apiConfiguration.actModeClineModelInfo) ||
|
||||
openRouterModelInfo
|
||||
const clinePassModelId =
|
||||
mode === "plan" ? apiConfiguration.planModeClinePassModelId : apiConfiguration.actModeClinePassModelId
|
||||
const clinePassModelInfo =
|
||||
mode === "plan" ? apiConfiguration.planModeClinePassModelInfo : apiConfiguration.actModeClinePassModelInfo
|
||||
|
||||
return {
|
||||
// Core fields
|
||||
@@ -593,6 +621,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
openAiModelId: mode === "plan" ? apiConfiguration.planModeOpenAiModelId : apiConfiguration.actModeOpenAiModelId,
|
||||
openRouterModelId,
|
||||
clineModelId,
|
||||
clinePassModelId,
|
||||
groqModelId: mode === "plan" ? apiConfiguration.planModeGroqModelId : apiConfiguration.actModeGroqModelId,
|
||||
basetenModelId: mode === "plan" ? apiConfiguration.planModeBasetenModelId : apiConfiguration.actModeBasetenModelId,
|
||||
huggingFaceModelId:
|
||||
@@ -612,6 +641,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
|
||||
liteLlmModelInfo: mode === "plan" ? apiConfiguration.planModeLiteLlmModelInfo : apiConfiguration.actModeLiteLlmModelInfo,
|
||||
openRouterModelInfo,
|
||||
clineModelInfo,
|
||||
clinePassModelInfo,
|
||||
requestyModelInfo:
|
||||
mode === "plan" ? apiConfiguration.planModeRequestyModelInfo : apiConfiguration.actModeRequestyModelInfo,
|
||||
groqModelInfo: mode === "plan" ? apiConfiguration.planModeGroqModelInfo : apiConfiguration.actModeGroqModelInfo,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Webview-side feature flag identifiers.
|
||||
*
|
||||
* Keep these in sync with the `FeatureFlag` enum in
|
||||
* `apps/vscode/src/shared/services/feature-flags/feature-flags.ts`.
|
||||
* They are duplicated here as plain string constants because the extension-side
|
||||
* enum module imports Node/extension-only dependencies that do not resolve in
|
||||
* the webview bundle.
|
||||
*/
|
||||
|
||||
/** Enables Cline Pass provider/model exposure (settings + onboarding). */
|
||||
export const CLINE_PASS_FEATURE_FLAG = "ext-cline-pass"
|
||||
@@ -777,7 +777,10 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
// Auto-refresh Cline models when provider is cline
|
||||
useEffect(() => {
|
||||
const hasClineProvider =
|
||||
state.apiConfiguration?.actModeApiProvider === "cline" || state.apiConfiguration?.planModeApiProvider === "cline"
|
||||
state.apiConfiguration?.actModeApiProvider === "cline" ||
|
||||
state.apiConfiguration?.actModeApiProvider === "cline-pass" ||
|
||||
state.apiConfiguration?.planModeApiProvider === "cline" ||
|
||||
state.apiConfiguration?.planModeApiProvider === "cline-pass"
|
||||
if (hasClineProvider && clineModels === null) {
|
||||
refreshClineModels()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiConfiguration, ModelInfo, openRouterDefaultModelId } from "@shared/api"
|
||||
import { ApiConfiguration, clinePassDefaultModelId, clinePassModels, ModelInfo, openRouterDefaultModelId } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { getModeSpecificFields } from "@/components/settings/utils/providerUtils"
|
||||
|
||||
@@ -71,6 +71,7 @@ export function validateApiConfiguration(currentMode: Mode, apiConfiguration?: A
|
||||
}
|
||||
break
|
||||
case "cline":
|
||||
case "cline-pass":
|
||||
break
|
||||
case "openai-codex":
|
||||
// Authentication is handled via OAuth, not API key
|
||||
@@ -191,7 +192,7 @@ export function validateModelId(
|
||||
if (apiConfiguration) {
|
||||
const { apiProvider, openRouterModelId, clineModelId } = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
switch (apiProvider) {
|
||||
case "openrouter":
|
||||
case "openrouter": {
|
||||
const modelId = openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
||||
if (!modelId) {
|
||||
return "You must provide a model ID."
|
||||
@@ -201,7 +202,8 @@ export function validateModelId(
|
||||
return "The model ID you provided is not available. Please choose a different model."
|
||||
}
|
||||
break
|
||||
case "cline":
|
||||
}
|
||||
case "cline": {
|
||||
const clineResolvedModelId = clineModelId || openRouterDefaultModelId
|
||||
if (!clineResolvedModelId) {
|
||||
return "You must provide a model ID."
|
||||
@@ -210,6 +212,22 @@ export function validateModelId(
|
||||
return "The model ID you provided is not available. Please choose a different model."
|
||||
}
|
||||
break
|
||||
}
|
||||
case "cline-pass": {
|
||||
const clinePassModelId =
|
||||
currentMode === "plan" ? apiConfiguration.planModeClinePassModelId : apiConfiguration.actModeClinePassModelId
|
||||
const clinePassResolvedModelId = clinePassModelId || clinePassDefaultModelId
|
||||
if (!clinePassResolvedModelId) {
|
||||
return "You must provide a model ID."
|
||||
}
|
||||
if (
|
||||
!Object.keys(clinePassModels).includes(clinePassResolvedModelId) &&
|
||||
!clinePassResolvedModelId.startsWith("cline-pass/")
|
||||
) {
|
||||
return "The model ID you provided is not available. Please choose a different model."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
Reference in New Issue
Block a user