mirror of
https://github.com/cline/cline.git
synced 2026-09-11 05:47:07 +08:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0555318b9e | ||
|
|
490dca884d | ||
|
|
51265f211c | ||
|
|
002841c199 | ||
|
|
7b0b7b7224 | ||
|
|
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 };
|
||||
}
|
||||
@@ -2460,6 +2466,9 @@ export class Task {
|
||||
const quotaExceeded = clineError.isErrorType(
|
||||
ClineErrorType.QuotaExceeded,
|
||||
);
|
||||
const isEntitlementError = clineError.isErrorType(
|
||||
ClineErrorType.Entitlement,
|
||||
);
|
||||
|
||||
// Check if this is a Cline provider insufficient credits error - don't auto-retry these
|
||||
const isClineProviderInsufficientCredits = (() => {
|
||||
@@ -2485,6 +2494,7 @@ export class Task {
|
||||
!isAuthError &&
|
||||
!isSpendLimitError &&
|
||||
!quotaExceeded &&
|
||||
!isEntitlementError &&
|
||||
this.taskState.autoRetryAttempts < 3;
|
||||
if (shouldRetry) {
|
||||
// Auto-retry enabled with max 3 attempts: automatically approve the retry
|
||||
@@ -2546,7 +2556,8 @@ export class Task {
|
||||
!isClineProviderInsufficientCredits &&
|
||||
!isAuthError &&
|
||||
!isSpendLimitError &&
|
||||
!quotaExceeded;
|
||||
!quotaExceeded &&
|
||||
!isEntitlementError;
|
||||
if (showRetry) {
|
||||
await this.say(
|
||||
"error_retry",
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum ClineErrorType {
|
||||
Balance = "balance",
|
||||
SpendLimit = "spendLimit",
|
||||
QuotaExceeded = "quotaExceeded",
|
||||
Entitlement = "entitlement",
|
||||
}
|
||||
|
||||
interface ErrorDetails {
|
||||
@@ -152,6 +153,11 @@ export class ClineError extends Error {
|
||||
return ClineErrorType.SpendLimit
|
||||
}
|
||||
|
||||
// Must be checked before the generic auth check since these are returned as 403
|
||||
if (code === "ENTITLEMENT_ERROR" || details?.code === "ENTITLEMENT_ERROR") {
|
||||
return ClineErrorType.Entitlement
|
||||
}
|
||||
|
||||
// Check auth errors
|
||||
const isAuthStatus = status !== undefined && status > 400 && status < 429
|
||||
if (code === "ERR_BAD_REQUEST" || err instanceof AuthInvalidTokenError || isAuthStatus) {
|
||||
|
||||
@@ -8,5 +8,50 @@ describe("ClineError", () => {
|
||||
const err = new ClineError({ message: "Inference cap reached", code: "INFERENCE_CAP_ERROR" })
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.QuotaExceeded)
|
||||
})
|
||||
|
||||
it("should return Entitlement when code is ENTITLEMENT_ERROR", () => {
|
||||
const err = new ClineError({
|
||||
message: "403 Error 403: the user is not subscribed to required model plan",
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
status: 403,
|
||||
})
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.Entitlement)
|
||||
})
|
||||
|
||||
it("should return Entitlement when details.code is ENTITLEMENT_ERROR", () => {
|
||||
const err = new ClineError({
|
||||
message: "403 Error 403: the user is not subscribed to required model plan",
|
||||
status: 403,
|
||||
details: { code: "ENTITLEMENT_ERROR", message: "Error 403: the user is not subscribed to required model plan" },
|
||||
})
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.Entitlement)
|
||||
})
|
||||
|
||||
it("should prefer Entitlement over Auth for 403 ENTITLEMENT_ERROR", () => {
|
||||
// status 403 would otherwise be classified as Auth; the entitlement code must win.
|
||||
const err = new ClineError({
|
||||
message: "403 Error 403: the user is not subscribed to required model plan",
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
status: 403,
|
||||
})
|
||||
ClineError.getErrorType(err)!.should.not.equal(ClineErrorType.Auth)
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.Entitlement)
|
||||
})
|
||||
|
||||
it("should return Entitlement for the real Cline 403 provider error shape (nested error object)", () => {
|
||||
// ClineError maps `error.error` into `details`, so `details.code` drives classification.
|
||||
const err = new ClineError(
|
||||
{
|
||||
status: 403,
|
||||
error: {
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
message: "Error 403: the user is not subscribed to required model plan",
|
||||
},
|
||||
},
|
||||
"cline-pass/glm-5.1",
|
||||
"cline-pass",
|
||||
)
|
||||
ClineError.getErrorType(err)!.should.equal(ClineErrorType.Entitlement)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -7,6 +7,7 @@ import deepEqual from "fast-deep-equal"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useInterval } from "react-use"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { APP_BASE_URL } from "@/constants"
|
||||
import { type ClineUser, handleSignOut } from "@/context/ClineAuthContext"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
@@ -229,7 +230,7 @@ export const ClineAccountView = ({ clineUser, userOrganizations, activeOrganizat
|
||||
fetchCreditBalance(dropdownValue)
|
||||
}, 60000)
|
||||
|
||||
const clineUrl = appBaseUrl || "https://app.cline.bot"
|
||||
const clineUrl = appBaseUrl || APP_BASE_URL
|
||||
|
||||
// Fetch balance on mount
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AskResponseRequest } from "@shared/proto/cline/task"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useEffect, useMemo, useState } from "react"
|
||||
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
|
||||
import { APP_BASE_URL } from "@/constants"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
import { AccountServiceClient, TaskServiceClient } from "@/services/grpc-client"
|
||||
|
||||
@@ -14,8 +15,8 @@ interface CreditLimitErrorProps {
|
||||
}
|
||||
|
||||
const DEFAULT_BUY_CREDITS_URL = {
|
||||
USER: "https://app.cline.bot/dashboard/account?tab=credits&redirect=true",
|
||||
ORG: "https://app.cline.bot/dashboard/organization?tab=credits&redirect=true",
|
||||
USER: `${APP_BASE_URL}/dashboard/account?tab=credits&redirect=true`,
|
||||
ORG: `${APP_BASE_URL}/dashboard/organization?tab=credits&redirect=true`,
|
||||
}
|
||||
|
||||
const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import EntitlementError from "./EntitlementError"
|
||||
|
||||
// Mocks are mutated per-test to simulate different auth/environment states.
|
||||
const mockAuth: { clineUser: { appBaseUrl?: string } | null } = { clineUser: null }
|
||||
const mockExtensionState: { environment?: string } = { environment: undefined }
|
||||
|
||||
vi.mock("@/context/ClineAuthContext", () => ({
|
||||
useClineAuth: () => mockAuth,
|
||||
}))
|
||||
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
useExtensionState: () => mockExtensionState,
|
||||
}))
|
||||
|
||||
const askResponseMock = vi.fn()
|
||||
vi.mock("@/services/grpc-client", () => ({
|
||||
TaskServiceClient: {
|
||||
askResponse: (...args: unknown[]) => askResponseMock(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
const getSubscribeHref = () => screen.getByRole("link", { name: /get cline pass/i }).getAttribute("href")
|
||||
|
||||
describe("EntitlementError", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockAuth.clineUser = null
|
||||
mockExtensionState.environment = undefined
|
||||
})
|
||||
|
||||
it("shows the friendly headline regardless of the backend message", () => {
|
||||
render(<EntitlementError message="Error 403: the user is not subscribed to required model plan" />)
|
||||
expect(screen.getByText("This model requires a Cline Pass subscription.")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("surfaces the backend detail as muted support text when it differs from the headline", () => {
|
||||
render(<EntitlementError message="Error 403: the user is not subscribed to required model plan" />)
|
||||
expect(screen.getByText("Error 403: the user is not subscribed to required model plan")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not duplicate the headline when no backend detail is provided", () => {
|
||||
render(<EntitlementError />)
|
||||
expect(screen.getAllByText("This model requires a Cline Pass subscription.")).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("defaults the subscribe link to production when no auth/environment is available", () => {
|
||||
render(<EntitlementError />)
|
||||
expect(getSubscribeHref()).toBe("https://app.cline.bot/dashboard/subscription")
|
||||
})
|
||||
|
||||
it("prefers the authenticated user's app base URL (staging)", () => {
|
||||
mockAuth.clineUser = { appBaseUrl: "https://staging-app.cline.bot" }
|
||||
render(<EntitlementError />)
|
||||
expect(getSubscribeHref()).toBe("https://staging-app.cline.bot/dashboard/subscription")
|
||||
})
|
||||
|
||||
it("falls back to the current environment when the user app base URL is unavailable (staging)", () => {
|
||||
mockExtensionState.environment = "staging"
|
||||
render(<EntitlementError />)
|
||||
expect(getSubscribeHref()).toBe("https://staging-app.cline.bot/dashboard/subscription")
|
||||
})
|
||||
|
||||
it("sends yesButtonClicked when Retry Request is clicked", () => {
|
||||
render(<EntitlementError />)
|
||||
// VSCodeButton has no ARIA role in jsdom; click by label text instead.
|
||||
fireEvent.click(screen.getByText("Retry Request"))
|
||||
expect(askResponseMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import { AskResponseRequest } from "@shared/proto/cline/task"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React from "react"
|
||||
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
|
||||
import { getAppBaseUrl } from "@/constants"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { TaskServiceClient } from "@/services/grpc-client"
|
||||
|
||||
interface EntitlementErrorProps {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const CLINE_PASS_SUBSCRIBE_PATH = "/dashboard/subscription"
|
||||
|
||||
const HEADLINE = "This model requires a Cline Pass subscription."
|
||||
|
||||
const EntitlementError: React.FC<EntitlementErrorProps> = ({ message }) => {
|
||||
const { clineUser } = useClineAuth()
|
||||
const { environment } = useExtensionState()
|
||||
const appBaseUrl = clineUser?.appBaseUrl || getAppBaseUrl(environment)
|
||||
const subscribeUrl = new URL(CLINE_PASS_SUBSCRIBE_PATH, appBaseUrl).toString()
|
||||
const backendDetail = message && message !== HEADLINE ? message : undefined
|
||||
|
||||
return (
|
||||
<div className="p-2 border-none rounded-md mb-2 bg-(--vscode-textBlockQuote-background)">
|
||||
<div className="mb-3">
|
||||
<div className="text-error mb-2">{HEADLINE}</div>
|
||||
<div className="text-(--vscode-descriptionForeground) text-xs">
|
||||
Subscribe to Cline Pass to use this model, then retry your request.
|
||||
</div>
|
||||
{backendDetail && (
|
||||
<div className="text-(--vscode-descriptionForeground) text-xs mt-1 opacity-80 wrap-anywhere">
|
||||
{backendDetail}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<VSCodeButtonLink className="w-full mb-2" href={subscribeUrl}>
|
||||
<span className="codicon codicon-rocket mr-[6px] text-[14px]" />
|
||||
Get Cline Pass
|
||||
</VSCodeButtonLink>
|
||||
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "yesButtonClicked",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error invoking action:", error)
|
||||
}
|
||||
}}>
|
||||
<span className="codicon codicon-refresh mr-1.5" />
|
||||
Retry Request
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EntitlementError
|
||||
@@ -214,6 +214,32 @@ export const ClineSpendLimitMinimal: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
// Cline Pass entitlement error (user not subscribed to a required model plan)
|
||||
export const ClinePassEntitlementError: Story = {
|
||||
args: {
|
||||
message: createMockMessage(),
|
||||
errorType: "error",
|
||||
apiRequestFailedMessage: JSON.stringify({
|
||||
message: "403 Error 403: the user is not subscribed to required model plan",
|
||||
status: 403,
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
modelId: "cline-pass/glm-5.1",
|
||||
providerId: "cline-pass",
|
||||
details: {
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
message: "Error 403: the user is not subscribed to required model plan",
|
||||
},
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Cline Pass model returns a 403 ENTITLEMENT_ERROR when the user is not subscribed. Instead of dumping the raw JSON blob, a human-readable message with a 'Get Cline Pass' subscribe link and a retry button is shown.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Authentication-related errors with configurable scenarios
|
||||
export const AuthenticationErrors: Story = {
|
||||
args: {
|
||||
|
||||
@@ -19,6 +19,11 @@ vi.mock("@/components/chat/CreditLimitError", () => ({
|
||||
default: ({ message }: { message: string }) => <div data-testid="credit-limit-error">{message}</div>,
|
||||
}))
|
||||
|
||||
// Mock EntitlementError component
|
||||
vi.mock("@/components/chat/EntitlementError", () => ({
|
||||
default: ({ message }: { message: string }) => <div data-testid="entitlement-error">{message}</div>,
|
||||
}))
|
||||
|
||||
// Mock ClineError
|
||||
vi.mock("../../../../src/services/error/ClineError", () => ({
|
||||
ClineError: {
|
||||
@@ -28,6 +33,7 @@ vi.mock("../../../../src/services/error/ClineError", () => ({
|
||||
Balance: "balance",
|
||||
RateLimit: "rateLimit",
|
||||
Auth: "auth",
|
||||
Entitlement: "entitlement",
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -128,6 +134,38 @@ describe("ErrorRow", () => {
|
||||
expect(screen.getByText("Inference cap reached")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders entitlement error with the detail message instead of a raw JSON blob", async () => {
|
||||
const mockClineError = {
|
||||
message: "403 Error 403: the user is not subscribed to required model plan",
|
||||
isErrorType: vi.fn((type) => type === "entitlement"),
|
||||
providerId: "cline-pass",
|
||||
_error: {
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
details: {
|
||||
code: "ENTITLEMENT_ERROR",
|
||||
message: "Error 403: the user is not subscribed to required model plan",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const { ClineError } = await import("../../../../src/services/error/ClineError")
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
|
||||
|
||||
render(
|
||||
<ErrorRow
|
||||
apiRequestFailedMessage='{"message":"403 Error 403...","code":"ENTITLEMENT_ERROR"}'
|
||||
errorType="error"
|
||||
message={mockMessage}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Renders the friendly EntitlementError component with the human-readable detail message...
|
||||
expect(screen.getByTestId("entitlement-error")).toBeInTheDocument()
|
||||
expect(screen.getByText("Error 403: the user is not subscribed to required model plan")).toBeInTheDocument()
|
||||
// ...and does not dump the raw JSON blob or the [CLINE-PASS] ENTITLEMENT_ERROR header.
|
||||
expect(screen.queryByText(/ENTITLEMENT_ERROR/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders friendly logged-out message and sign in button when user is not signed in", async () => {
|
||||
const mockClineError = {
|
||||
message: "Authentication failed",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { memo } from "react"
|
||||
import CreditLimitError from "@/components/chat/CreditLimitError"
|
||||
import EntitlementError from "@/components/chat/EntitlementError"
|
||||
import SpendLimitError from "@/components/chat/SpendLimitError"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useClineAuth, useClineSignIn } from "@/context/ClineAuthContext"
|
||||
@@ -61,6 +62,11 @@ const ErrorRow = memo(({ message, errorType, apiRequestFailedMessage, apiReqStre
|
||||
)
|
||||
}
|
||||
|
||||
if (clineError?.isErrorType(ClineErrorType.Entitlement)) {
|
||||
const detailMessage = clineError?._error?.details?.message || errorMessage
|
||||
return <EntitlementError message={detailMessage} />
|
||||
}
|
||||
|
||||
if (clineError?.isErrorType(ClineErrorType.RateLimit)) {
|
||||
return (
|
||||
<p className="m-0 whitespace-pre-wrap text-error wrap-anywhere">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { normalizeApiConfiguration } from "@/components/settings/utils/providerU
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
|
||||
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 +20,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"
|
||||
@@ -56,6 +58,8 @@ import { XaiProvider } from "./providers/XaiProvider"
|
||||
import { ZAiProvider } from "./providers/ZAiProvider"
|
||||
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
|
||||
|
||||
const CLINE_PASS_FEATURE_FLAG = "ext-cline-pass"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showModelOptions: boolean
|
||||
apiErrorMessage?: string
|
||||
@@ -99,8 +103,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 +146,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 +162,7 @@ const ApiOptions = ({
|
||||
}
|
||||
|
||||
return providers
|
||||
}, [remoteConfigSettings])
|
||||
}, [isClinePassEnabled, remoteConfigSettings])
|
||||
|
||||
const currentProviderLabel = useMemo(() => {
|
||||
return providerOptions.find((option) => option.value === selectedProvider)?.label || selectedProvider
|
||||
@@ -363,11 +371,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,
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import { Environment } from "../../src/shared/config-types"
|
||||
|
||||
export const LINKS = {
|
||||
DOCUMENTATION: {
|
||||
REMOTE_MCP_SERVER_DOCS: "https://docs.cline.bot/mcp/connecting-to-a-remote-server",
|
||||
LOCAL_MCP_SERVER_DOCS: "https://docs.cline.bot/mcp/configuring-mcp-servers#editing-mcp-settings-files",
|
||||
},
|
||||
}
|
||||
|
||||
export const APP_BASE_URL = "https://app.cline.bot"
|
||||
|
||||
// Keep in sync with ClineEndpoint.getEnvironment() in apps/vscode/src/config.ts.
|
||||
const APP_BASE_URL_BY_ENVIRONMENT: Record<Environment, string> = {
|
||||
[Environment.production]: APP_BASE_URL,
|
||||
[Environment.staging]: "https://staging-app.cline.bot",
|
||||
[Environment.local]: "http://localhost:3000",
|
||||
[Environment.selfHosted]: APP_BASE_URL,
|
||||
}
|
||||
|
||||
export function getAppBaseUrl(environment?: Environment): string {
|
||||
return (environment && APP_BASE_URL_BY_ENVIRONMENT[environment]) || APP_BASE_URL
|
||||
}
|
||||
|
||||
@@ -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