mirror of
https://github.com/cline/cline.git
synced 2026-09-09 15:02:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e40a9d6072 | ||
|
|
d270941bcd | ||
|
|
86e8b61206 |
@@ -1,11 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [4.0.1]
|
||||
|
||||
### Changed
|
||||
|
||||
- Roll the stable VS Code extension back to the pre-SDK-migration codebase to resolve regressions reported in 4.0.0. This release ships the 3.89.2 extension code under a higher version number so existing 4.0.0 users receive the update. SDK-migration work continues separately on `main`.
|
||||
|
||||
## [3.89.2]
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "4.0.1",
|
||||
"version": "3.89.2",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -921,7 +921,6 @@ export class Controller {
|
||||
const environment = clineConfig.environment
|
||||
const banners = BannerService.get().getActiveBanners() ?? []
|
||||
const welcomeBanners = BannerService.get().getWelcomeBanners() ?? []
|
||||
const modelsDevProviderModels = this.stateManager.getModelsDevProviderModelsCache() ?? undefined
|
||||
|
||||
// Check OpenAI Codex authentication status
|
||||
const { openAiCodexOAuthManager } = await import("@/integrations/openai-codex/oauth")
|
||||
@@ -970,7 +969,6 @@ export class Controller {
|
||||
isNewUser,
|
||||
welcomeViewCompleted,
|
||||
onboardingModels,
|
||||
modelsDevProviderModels,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
maxConsecutiveMistakes,
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { applyModelsDevProviderModels, type ModelsDevProviderModels, normalizeModelsDevProviderModels } from "@shared/models-dev"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
export const MODELS_DEV_CATALOG_URL = "https://models.dev/api.json"
|
||||
|
||||
let pendingRefresh: Promise<ModelsDevProviderModels> | null = null
|
||||
|
||||
export async function refreshModelsDevProviderModels(): Promise<ModelsDevProviderModels> {
|
||||
const cache = StateManager.get().getModelsDevProviderModelsCache()
|
||||
if (cache) {
|
||||
applyModelsDevProviderModels(cache)
|
||||
return cache
|
||||
}
|
||||
|
||||
if (pendingRefresh) {
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
pendingRefresh = (async () => {
|
||||
try {
|
||||
return await fetchAndCacheModelsDevProviderModels()
|
||||
} finally {
|
||||
pendingRefresh = null
|
||||
}
|
||||
})()
|
||||
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
async function fetchAndCacheModelsDevProviderModels(): Promise<ModelsDevProviderModels> {
|
||||
const modelsDevProviderModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.modelsDevProviderModels)
|
||||
|
||||
let providerModels: ModelsDevProviderModels = {}
|
||||
try {
|
||||
const response = await axios.get(MODELS_DEV_CATALOG_URL, getAxiosSettings())
|
||||
providerModels = normalizeModelsDevProviderModels(response.data)
|
||||
|
||||
if (Object.keys(providerModels).length === 0) {
|
||||
throw new Error("No supported models.dev provider models found")
|
||||
}
|
||||
|
||||
await fs.writeFile(modelsDevProviderModelsFilePath, JSON.stringify(providerModels))
|
||||
Logger.log("models.dev provider models fetched and saved")
|
||||
} catch (error) {
|
||||
Logger.error("Error fetching models.dev provider models:", error)
|
||||
|
||||
const cachedModels = await readModelsDevProviderModelsFromCache()
|
||||
if (cachedModels && Object.keys(cachedModels).length > 0) {
|
||||
providerModels = cachedModels
|
||||
Logger.log("Loaded models.dev provider models from cache")
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(providerModels).length > 0) {
|
||||
applyModelsDevProviderModels(providerModels)
|
||||
StateManager.get().setModelsDevProviderModelsCache(providerModels)
|
||||
}
|
||||
|
||||
return providerModels
|
||||
}
|
||||
|
||||
export async function readModelsDevProviderModelsFromCache(): Promise<ModelsDevProviderModels | undefined> {
|
||||
try {
|
||||
const modelsDevProviderModelsFilePath = path.join(
|
||||
await ensureCacheDirectoryExists(),
|
||||
GlobalFileNames.modelsDevProviderModels,
|
||||
)
|
||||
const fileExists = await fileExistsAtPath(modelsDevProviderModelsFilePath)
|
||||
if (fileExists) {
|
||||
const fileContents = await fs.readFile(modelsDevProviderModelsFilePath, "utf8")
|
||||
return JSON.parse(fileContents)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error reading cached models.dev provider models:", error)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { refreshClineModels } from "../models/refreshClineModels"
|
||||
import { refreshGroqModels } from "../models/refreshGroqModels"
|
||||
import { refreshHicapModels } from "../models/refreshHicapModels"
|
||||
import { refreshLiteLlmModels } from "../models/refreshLiteLlmModels"
|
||||
import { refreshModelsDevProviderModels } from "../models/refreshModelsDevProviderModels"
|
||||
import { refreshOpenRouterModels } from "../models/refreshOpenRouterModels"
|
||||
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
|
||||
|
||||
@@ -29,14 +28,6 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
sendOpenRouterModelsEvent(OpenRouterCompatibleModelInfo.create({ models: lastCachedModels }))
|
||||
}
|
||||
|
||||
refreshModelsDevProviderModels()
|
||||
.then(async (models) => {
|
||||
if (models && Object.keys(models).length > 0) {
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
})
|
||||
.catch((error) => Logger.error("Failed to refresh models.dev provider models:", error))
|
||||
|
||||
// Refresh OpenRouter models from API
|
||||
refreshOpenRouterModels(controller).then(async (models) => {
|
||||
if (models && Object.keys(models).length > 0) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ApiConfiguration, ModelInfo } from "@shared/api"
|
||||
import type { ModelsDevProviderModels } from "@shared/models-dev"
|
||||
import {
|
||||
ApiHandlerSettingsKeys,
|
||||
type GlobalState,
|
||||
@@ -103,7 +102,6 @@ export class StateManager {
|
||||
liteLlmModels: null,
|
||||
vercelModels: null,
|
||||
}
|
||||
private modelsDevProviderModelsCache: { data: ModelsDevProviderModels; timestamp: number } | null = null
|
||||
|
||||
// Debounced persistence state
|
||||
private pendingGlobalState = new Set<GlobalStateAndSettingsKey>()
|
||||
@@ -504,24 +502,6 @@ export class StateManager {
|
||||
return cached.data
|
||||
}
|
||||
|
||||
setModelsDevProviderModelsCache(models: ModelsDevProviderModels): void {
|
||||
this.modelsDevProviderModelsCache = { data: models, timestamp: Date.now() }
|
||||
}
|
||||
|
||||
getModelsDevProviderModelsCache(): ModelsDevProviderModels | null {
|
||||
const cached = this.modelsDevProviderModelsCache
|
||||
if (!cached) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Date.now() - cached.timestamp > this.MODEL_CACHE_TTL_MS) {
|
||||
this.modelsDevProviderModelsCache = null
|
||||
return null
|
||||
}
|
||||
|
||||
return cached.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model info by provider and model ID (from in-memory cache)
|
||||
*/
|
||||
|
||||
@@ -51,7 +51,6 @@ export const GlobalFileNames = {
|
||||
uiMessages: "ui_messages.json",
|
||||
clineRecommendedModels: "cline_recommended_models.json",
|
||||
clineModels: "cline_models.json",
|
||||
modelsDevProviderModels: "models_dev_provider_models.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
vercelAiGatewayModels: "vercel_ai_gateway_models.json",
|
||||
groqModels: "groq_models.json",
|
||||
|
||||
@@ -13,7 +13,6 @@ import { FocusChainSettings } from "./FocusChainSettings"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpDisplayMode } from "./McpDisplayMode"
|
||||
import { ClineMessageModelInfo } from "./messages"
|
||||
import type { ModelsDevProviderModels } from "./models-dev"
|
||||
import { OnboardingModelGroup } from "./proto/cline/state"
|
||||
import { Mode } from "./storage/types"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
@@ -41,7 +40,6 @@ export interface ExtensionState {
|
||||
isNewUser: boolean
|
||||
welcomeViewCompleted: boolean
|
||||
onboardingModels: OnboardingModelGroup | undefined
|
||||
modelsDevProviderModels?: ModelsDevProviderModels
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { expect } from "chai"
|
||||
import { afterEach, describe, it } from "mocha"
|
||||
import { anthropicModels, type ModelInfo } from "./api"
|
||||
import {
|
||||
applyModelsDevProviderModels,
|
||||
type ModelsDevPayload,
|
||||
mergeModelsDevModels,
|
||||
normalizeModelsDevProviderModels,
|
||||
} from "./models-dev"
|
||||
|
||||
describe("models.dev static provider augmentation", () => {
|
||||
const augmentedAnthropicModelId = "claude-test-model-from-models-dev"
|
||||
|
||||
afterEach(() => {
|
||||
delete (anthropicModels as Record<string, ModelInfo>)[augmentedAnthropicModelId]
|
||||
})
|
||||
|
||||
it("normalizes supported models.dev models and filters unsupported entries", () => {
|
||||
const payload: ModelsDevPayload = {
|
||||
anthropic: {
|
||||
models: {
|
||||
[augmentedAnthropicModelId]: {
|
||||
name: "Claude Test",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }],
|
||||
release_date: "2026-01-01",
|
||||
limit: {
|
||||
context: 200_000,
|
||||
output: 64_000,
|
||||
},
|
||||
cost: {
|
||||
input: 3,
|
||||
output: 15,
|
||||
cache_read: 0.3,
|
||||
cache_write: 3.75,
|
||||
},
|
||||
modalities: {
|
||||
input: ["text", "image"],
|
||||
},
|
||||
},
|
||||
"claude-deprecated": {
|
||||
tool_call: true,
|
||||
status: "deprecated",
|
||||
},
|
||||
"claude-no-tools": {
|
||||
tool_call: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const providerModels = normalizeModelsDevProviderModels(payload)
|
||||
const model = providerModels.anthropic?.[augmentedAnthropicModelId]
|
||||
|
||||
expect(model).to.not.equal(undefined)
|
||||
expect(model?.name).to.equal("Claude Test")
|
||||
expect(model?.contextWindow).to.equal(200_000)
|
||||
expect(model?.maxTokens).to.equal(64_000)
|
||||
expect(model?.supportsImages).to.equal(true)
|
||||
expect(model?.supportsPromptCache).to.equal(true)
|
||||
expect(model?.supportsReasoning).to.equal(true)
|
||||
expect(model?.supportsReasoningEffort).to.equal(true)
|
||||
expect(model?.inputPrice).to.equal(3)
|
||||
expect(providerModels.anthropic?.["claude-deprecated"]).to.equal(undefined)
|
||||
expect(providerModels.anthropic?.["claude-no-tools"]).to.equal(undefined)
|
||||
})
|
||||
|
||||
it("keeps hardcoded model info while appending missing models.dev ids", () => {
|
||||
const staticModels: Record<string, ModelInfo> = {
|
||||
existing: {
|
||||
maxTokens: 1,
|
||||
contextWindow: 1,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1,
|
||||
outputPrice: 1,
|
||||
},
|
||||
}
|
||||
const modelsDevModels: Record<string, ModelInfo> = {
|
||||
existing: {
|
||||
maxTokens: 2,
|
||||
contextWindow: 2,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2,
|
||||
outputPrice: 2,
|
||||
},
|
||||
added: {
|
||||
maxTokens: 3,
|
||||
contextWindow: 3,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 3,
|
||||
},
|
||||
}
|
||||
|
||||
const merged = mergeModelsDevModels(staticModels, modelsDevModels)
|
||||
|
||||
expect(merged.existing.maxTokens).to.equal(1)
|
||||
expect(merged.added.maxTokens).to.equal(3)
|
||||
expect(Object.keys(merged)).to.deep.equal(["existing", "added"])
|
||||
})
|
||||
|
||||
it("applies missing models.dev ids to existing static provider maps", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: 64_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 3,
|
||||
outputPrice: 15,
|
||||
cacheReadsPrice: 0.3,
|
||||
cacheWritesPrice: 3.75,
|
||||
}
|
||||
|
||||
applyModelsDevProviderModels({
|
||||
anthropic: {
|
||||
[augmentedAnthropicModelId]: modelInfo,
|
||||
},
|
||||
})
|
||||
|
||||
expect((anthropicModels as Record<string, ModelInfo>)[augmentedAnthropicModelId]).to.deep.equal(modelInfo)
|
||||
})
|
||||
})
|
||||
@@ -1,236 +0,0 @@
|
||||
import {
|
||||
type ApiProvider,
|
||||
anthropicModels,
|
||||
bedrockModels,
|
||||
cerebrasModels,
|
||||
deepSeekModels,
|
||||
fireworksModels,
|
||||
geminiModels,
|
||||
huggingFaceModels,
|
||||
internationalZAiModels,
|
||||
type ModelInfo,
|
||||
mainlandZAiModels,
|
||||
minimaxModels,
|
||||
mistralModels,
|
||||
moonshotModels,
|
||||
nebiusModels,
|
||||
nousResearchModels,
|
||||
openAiNativeModels,
|
||||
sambanovaModels,
|
||||
vertexModels,
|
||||
wandbModels,
|
||||
xaiModels,
|
||||
} from "./api"
|
||||
|
||||
export type ModelsDevModelInfo = ModelInfo & {
|
||||
releaseDate?: string
|
||||
family?: string
|
||||
supportsReasoningEffort?: boolean
|
||||
supportsTools?: boolean
|
||||
}
|
||||
|
||||
export type ModelsDevProviderModels = Partial<Record<ApiProvider, Record<string, ModelsDevModelInfo>>>
|
||||
|
||||
export interface ModelsDevModel {
|
||||
name?: string
|
||||
tool_call?: boolean
|
||||
reasoning?: boolean
|
||||
structured_output?: boolean
|
||||
temperature?: boolean
|
||||
reasoning_options?: {
|
||||
type?: string
|
||||
values?: string[]
|
||||
min?: number
|
||||
}[]
|
||||
release_date?: string
|
||||
family?: string
|
||||
limit?: {
|
||||
context?: number
|
||||
input?: number
|
||||
output?: number
|
||||
}
|
||||
cost?: {
|
||||
input?: number
|
||||
output?: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
}
|
||||
modalities?: {
|
||||
input?: string[]
|
||||
}
|
||||
status?: string
|
||||
}
|
||||
|
||||
export type ModelsDevPayload = Record<string, { models?: Record<string, ModelsDevModel> }>
|
||||
|
||||
const DEFAULT_MAX_TOKENS = 4096
|
||||
|
||||
const MODELS_DEV_PROVIDER_KEY_MAP: Record<string, ApiProvider> = {
|
||||
"amazon-bedrock": "bedrock",
|
||||
anthropic: "anthropic",
|
||||
cerebras: "cerebras",
|
||||
deepseek: "deepseek",
|
||||
"fireworks-ai": "fireworks",
|
||||
google: "gemini",
|
||||
"google-vertex": "vertex",
|
||||
huggingface: "huggingface",
|
||||
minimax: "minimax",
|
||||
mistral: "mistral",
|
||||
moonshotai: "moonshot",
|
||||
nebius: "nebius",
|
||||
"nous-research": "nousResearch",
|
||||
openai: "openai-native",
|
||||
sambanova: "sambanova",
|
||||
wandb: "wandb",
|
||||
xai: "xai",
|
||||
zai: "zai",
|
||||
}
|
||||
|
||||
const STATIC_MODELS_BY_PROVIDER: Partial<Record<ApiProvider, Record<string, ModelInfo>>> = {
|
||||
anthropic: anthropicModels as Record<string, ModelInfo>,
|
||||
bedrock: bedrockModels as Record<string, ModelInfo>,
|
||||
cerebras: cerebrasModels as Record<string, ModelInfo>,
|
||||
deepseek: deepSeekModels as Record<string, ModelInfo>,
|
||||
fireworks: fireworksModels as Record<string, ModelInfo>,
|
||||
gemini: geminiModels as Record<string, ModelInfo>,
|
||||
huggingface: huggingFaceModels as Record<string, ModelInfo>,
|
||||
minimax: minimaxModels as Record<string, ModelInfo>,
|
||||
mistral: mistralModels as Record<string, ModelInfo>,
|
||||
moonshot: moonshotModels as Record<string, ModelInfo>,
|
||||
nebius: nebiusModels as Record<string, ModelInfo>,
|
||||
nousResearch: nousResearchModels as Record<string, ModelInfo>,
|
||||
"openai-native": openAiNativeModels as Record<string, ModelInfo>,
|
||||
sambanova: sambanovaModels as Record<string, ModelInfo>,
|
||||
vertex: vertexModels as Record<string, ModelInfo>,
|
||||
wandb: wandbModels as Record<string, ModelInfo>,
|
||||
xai: xaiModels as Record<string, ModelInfo>,
|
||||
zai: internationalZAiModels as Record<string, ModelInfo>,
|
||||
}
|
||||
|
||||
function parseReleaseDate(value: string | undefined): number {
|
||||
if (!value) {
|
||||
return Number.NEGATIVE_INFINITY
|
||||
}
|
||||
const timestamp = Date.parse(value)
|
||||
return Number.isNaN(timestamp) ? Number.NEGATIVE_INFINITY : timestamp
|
||||
}
|
||||
|
||||
function sortModelsByReleaseDate(models: Record<string, ModelsDevModelInfo>): Record<string, ModelsDevModelInfo> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(models).sort(([modelIdA, modelA], [modelIdB, modelB]) => {
|
||||
const releaseDateA = parseReleaseDate(modelA.releaseDate)
|
||||
const releaseDateB = parseReleaseDate(modelB.releaseDate)
|
||||
if (releaseDateA !== releaseDateB) {
|
||||
return releaseDateB - releaseDateA
|
||||
}
|
||||
return modelIdA.localeCompare(modelIdB)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function hasCachePricing(cost: ModelsDevModel["cost"]): boolean {
|
||||
return typeof cost?.cache_read === "number" || typeof cost?.cache_write === "number"
|
||||
}
|
||||
|
||||
function supportsReasoningEffort(model: ModelsDevModel): boolean {
|
||||
return model.reasoning_options?.some((option) => option.type === "effort") ?? false
|
||||
}
|
||||
|
||||
function toModelInfo(modelId: string, model: ModelsDevModel): ModelsDevModelInfo {
|
||||
const supportsPromptCache = hasCachePricing(model.cost)
|
||||
const supportsReasoning = model.reasoning === true
|
||||
const supportsEffort = supportsReasoningEffort(model)
|
||||
const info: ModelsDevModelInfo = {
|
||||
name: model.name || modelId,
|
||||
maxTokens: Math.floor(model.limit?.output ?? DEFAULT_MAX_TOKENS),
|
||||
contextWindow: model.limit?.context,
|
||||
supportsImages: model.modalities?.input?.includes("image") ?? false,
|
||||
supportsPromptCache,
|
||||
supportsReasoning,
|
||||
inputPrice: model.cost?.input ?? 0,
|
||||
outputPrice: model.cost?.output ?? 0,
|
||||
cacheReadsPrice: model.cost?.cache_read,
|
||||
cacheWritesPrice: model.cost?.cache_write,
|
||||
description: "",
|
||||
thinkingConfig: supportsReasoning ? { maxBudget: model.limit?.output ?? DEFAULT_MAX_TOKENS } : undefined,
|
||||
releaseDate: model.release_date,
|
||||
family: model.family,
|
||||
supportsReasoningEffort: supportsEffort,
|
||||
supportsTools: model.tool_call === true,
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
function isSupportedModelsDevModel(model: ModelsDevModel): boolean {
|
||||
return model.tool_call === true && model.status !== "deprecated"
|
||||
}
|
||||
|
||||
export function normalizeModelsDevProviderModels(payload: ModelsDevPayload): ModelsDevProviderModels {
|
||||
const providerModels: ModelsDevProviderModels = {}
|
||||
|
||||
for (const [modelsDevProviderKey, providerId] of Object.entries(MODELS_DEV_PROVIDER_KEY_MAP)) {
|
||||
const sourceModels = payload[modelsDevProviderKey]?.models
|
||||
if (!sourceModels) {
|
||||
continue
|
||||
}
|
||||
|
||||
const models: Record<string, ModelsDevModelInfo> = {}
|
||||
for (const [modelId, model] of Object.entries(sourceModels)) {
|
||||
if (!isSupportedModelsDevModel(model)) {
|
||||
continue
|
||||
}
|
||||
models[modelId] = toModelInfo(modelId, model)
|
||||
}
|
||||
|
||||
if (Object.keys(models).length > 0) {
|
||||
providerModels[providerId] = sortModelsByReleaseDate(models)
|
||||
}
|
||||
}
|
||||
|
||||
return providerModels
|
||||
}
|
||||
|
||||
export function getStaticModelsForModelsDevProvider(providerId: ApiProvider): Record<string, ModelInfo> | undefined {
|
||||
return STATIC_MODELS_BY_PROVIDER[providerId]
|
||||
}
|
||||
|
||||
export function mergeModelsDevModels(
|
||||
staticModels: Record<string, ModelInfo>,
|
||||
modelsDevModels: Record<string, ModelInfo> | undefined,
|
||||
): Record<string, ModelInfo> {
|
||||
if (!modelsDevModels || Object.keys(modelsDevModels).length === 0) {
|
||||
return staticModels
|
||||
}
|
||||
|
||||
const additions = Object.fromEntries(Object.entries(modelsDevModels).filter(([modelId]) => !(modelId in staticModels)))
|
||||
return {
|
||||
...staticModels,
|
||||
...additions,
|
||||
}
|
||||
}
|
||||
|
||||
export function applyModelsDevProviderModels(providerModels: ModelsDevProviderModels | undefined): void {
|
||||
if (!providerModels) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const [providerId, models] of Object.entries(providerModels) as [ApiProvider, Record<string, ModelInfo>][]) {
|
||||
const staticModels = getStaticModelsForModelsDevProvider(providerId)
|
||||
if (!staticModels) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const [modelId, modelInfo] of Object.entries(models)) {
|
||||
if (!(modelId in staticModels)) {
|
||||
staticModels[modelId] = modelInfo
|
||||
}
|
||||
if (providerId === "zai") {
|
||||
const mainlandModels = mainlandZAiModels as Record<string, ModelInfo>
|
||||
if (!(modelId in mainlandModels)) {
|
||||
mainlandModels[modelId] = modelInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_PLATFORM, type ExtensionState } from "@shared/ExtensionMessage"
|
||||
import { DEFAULT_FOCUS_CHAIN_SETTINGS } from "@shared/FocusChainSettings"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE } from "@shared/McpDisplayMode"
|
||||
import { applyModelsDevProviderModels } from "@shared/models-dev"
|
||||
import type { UserInfo } from "@shared/proto/cline/account"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import type { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
|
||||
@@ -360,7 +359,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
if (response.stateJson) {
|
||||
try {
|
||||
const stateData = JSON.parse(response.stateJson) as ExtensionState
|
||||
applyModelsDevProviderModels(stateData.modelsDevProviderModels)
|
||||
setState((prevState) => {
|
||||
// Versioning logic for autoApprovalSettings
|
||||
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
|
||||
|
||||
@@ -399,6 +399,47 @@ describe("default search_codebase tool", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts object input with queries as a JSON-encoded string array", async () => {
|
||||
const execute = vi.fn(async (query: string) => `results:${query}`);
|
||||
const tool = createSearchTool(execute);
|
||||
|
||||
const result = await tool.execute(
|
||||
{
|
||||
queries: JSON.stringify(["createSearchTool", "run_commands"]),
|
||||
} as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
query: "createSearchTool",
|
||||
result: "results:createSearchTool",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
query: "run_commands",
|
||||
result: "results:run_commands",
|
||||
success: true,
|
||||
},
|
||||
]);
|
||||
expect(execute).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"createSearchTool",
|
||||
process.cwd(),
|
||||
expect.objectContaining({ iteration: 1 }),
|
||||
);
|
||||
expect(execute).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"run_commands",
|
||||
process.cwd(),
|
||||
expect.objectContaining({ iteration: 1 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("default apply_patch tool", () => {
|
||||
@@ -535,6 +576,111 @@ describe("default run_commands tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts object input with commands as a JSON-encoded string array", async () => {
|
||||
const execute = vi.fn(async (command: string | { command: string }) =>
|
||||
typeof command === "string" ? `ran:${command}` : `ran:${command.command}`,
|
||||
);
|
||||
const tool = createBashTool(execute);
|
||||
const command = "cd /repo && bunx tsc --noEmit --pretty 2>&1 | head -40";
|
||||
|
||||
const result = await tool.execute(
|
||||
{ commands: JSON.stringify([command]) } as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
query: command,
|
||||
result: `ran:${command}`,
|
||||
success: true,
|
||||
},
|
||||
]);
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
command,
|
||||
process.cwd(),
|
||||
expect.objectContaining({
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts JSON-encoded command arrays in the Windows shell tool", async () => {
|
||||
const execute = vi.fn(async (command: string | { command: string }) =>
|
||||
typeof command === "string" ? `ran:${command}` : `ran:${command.command}`,
|
||||
);
|
||||
const tool = createWindowsShellTool(execute);
|
||||
|
||||
const result = await tool.execute(
|
||||
{ commands: JSON.stringify(["git status --short"]) } as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
query: "git status --short",
|
||||
result: "ran:git status --short",
|
||||
success: true,
|
||||
},
|
||||
]);
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
"git status --short",
|
||||
process.cwd(),
|
||||
expect.objectContaining({
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts nested JSON-encoded commands payloads", async () => {
|
||||
const execute = vi.fn(async (command: string | { command: string }) =>
|
||||
typeof command === "string" ? `ran:${command}` : `ran:${command.command}`,
|
||||
);
|
||||
const tool = createBashTool(execute);
|
||||
|
||||
const result = await tool.execute(
|
||||
{
|
||||
commands: JSON.stringify({
|
||||
commands: JSON.stringify(["git status --short"]),
|
||||
}),
|
||||
} as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
query: "git status --short",
|
||||
result: "ran:git status --short",
|
||||
success: true,
|
||||
},
|
||||
]);
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
"git status --short",
|
||||
process.cwd(),
|
||||
expect.objectContaining({
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts common single-command aliases", async () => {
|
||||
const execute = vi.fn(
|
||||
async (command: string | { command: string }) =>
|
||||
@@ -1360,6 +1506,45 @@ describe("default read_files tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts object input with files as a JSON-encoded string array", async () => {
|
||||
const execute = vi.fn(
|
||||
async (request: { path: string }) => `content:${request.path}`,
|
||||
);
|
||||
const tool = createReadFilesTool(execute);
|
||||
|
||||
const result = await tool.execute(
|
||||
{ files: JSON.stringify(["/tmp/a.ts", "/tmp/b.ts"]) } as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
query: "/tmp/a.ts",
|
||||
result: "content:/tmp/a.ts",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
query: "/tmp/b.ts",
|
||||
result: "content:/tmp/b.ts",
|
||||
success: true,
|
||||
},
|
||||
]);
|
||||
expect(execute).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ path: "/tmp/a.ts" },
|
||||
expect.objectContaining({ iteration: 1 }),
|
||||
);
|
||||
expect(execute).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ path: "/tmp/b.ts" },
|
||||
expect.objectContaining({ iteration: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid union inputs before calling the executor", async () => {
|
||||
const execute = vi.fn(async () => "should not run");
|
||||
const tool = createReadFilesTool(execute);
|
||||
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
formatRunCommandQueryPreview,
|
||||
getEditorSizeError,
|
||||
getReadFileRangeError,
|
||||
normalizeJsonLikeReadFilesInput,
|
||||
normalizeJsonLikeRunCommandsInput,
|
||||
normalizeJsonLikeSearchCodebaseInput,
|
||||
normalizeRunCommandsInput,
|
||||
TimeoutError,
|
||||
withTimeout,
|
||||
@@ -118,10 +121,21 @@ function getHeredocDelimiter(command: string): string | undefined {
|
||||
return match?.[1] ?? match?.[2] ?? match?.[3];
|
||||
}
|
||||
|
||||
function coalesceSplitHeredocCommands(commands: string[]): string[] {
|
||||
const coalesced: string[] = [];
|
||||
function coalesceSplitHeredocCommands(commands: string[]): string[];
|
||||
function coalesceSplitHeredocCommands(
|
||||
commands: Array<string | StructuredCommandInput>,
|
||||
): Array<string | StructuredCommandInput>;
|
||||
function coalesceSplitHeredocCommands(
|
||||
commands: Array<string | StructuredCommandInput>,
|
||||
): Array<string | StructuredCommandInput> {
|
||||
const coalesced: Array<string | StructuredCommandInput> = [];
|
||||
for (let index = 0; index < commands.length; index += 1) {
|
||||
const command = commands[index];
|
||||
if (typeof command !== "string") {
|
||||
coalesced.push(command);
|
||||
continue;
|
||||
}
|
||||
|
||||
const delimiter = getHeredocDelimiter(command);
|
||||
if (!delimiter) {
|
||||
coalesced.push(command);
|
||||
@@ -130,7 +144,9 @@ function coalesceSplitHeredocCommands(commands: string[]): string[] {
|
||||
|
||||
const endIndex = commands.findIndex(
|
||||
(nextCommand, nextIndex) =>
|
||||
nextIndex > index && nextCommand.trim() === delimiter,
|
||||
nextIndex > index &&
|
||||
typeof nextCommand === "string" &&
|
||||
nextCommand.trim() === delimiter,
|
||||
);
|
||||
if (endIndex === -1) {
|
||||
coalesced.push(command);
|
||||
@@ -141,7 +157,9 @@ function coalesceSplitHeredocCommands(commands: string[]): string[] {
|
||||
while (index < endIndex) {
|
||||
index += 1;
|
||||
const nextCommand = commands[index];
|
||||
parts.push(nextCommand);
|
||||
if (typeof nextCommand === "string") {
|
||||
parts.push(nextCommand);
|
||||
}
|
||||
}
|
||||
coalesced.push(parts.join("\n"));
|
||||
}
|
||||
@@ -176,7 +194,10 @@ export function createReadFilesTool(
|
||||
retryable: true,
|
||||
maxRetries: 1,
|
||||
execute: async (input, context) => {
|
||||
const validate = validateWithZod(ReadFilesInputUnionSchema, input);
|
||||
const validate = validateWithZod(
|
||||
ReadFilesInputUnionSchema,
|
||||
normalizeJsonLikeReadFilesInput(input),
|
||||
);
|
||||
let requests: ReadFileRequest[];
|
||||
if (typeof validate === "string") {
|
||||
requests = [{ path: validate }];
|
||||
@@ -270,7 +291,10 @@ export function createSearchTool(
|
||||
maxRetries: 1,
|
||||
execute: async (input, context) => {
|
||||
// Validate input with Zod schema
|
||||
const validate = validateWithZod(SearchCodebaseUnionInputSchema, input);
|
||||
const validate = validateWithZod(
|
||||
SearchCodebaseUnionInputSchema,
|
||||
normalizeJsonLikeSearchCodebaseInput(input),
|
||||
);
|
||||
const queries = Array.isArray(validate)
|
||||
? validate
|
||||
: typeof validate === "object"
|
||||
@@ -336,7 +360,10 @@ export function createBashTool(
|
||||
retryable: false, // Shell commands often have side effects
|
||||
maxRetries: 0,
|
||||
execute: async (input, context) => {
|
||||
const validate = validateWithZod(RunCommandsInputUnionSchema, input);
|
||||
const validate = validateWithZod(
|
||||
RunCommandsInputUnionSchema,
|
||||
normalizeJsonLikeRunCommandsInput(input),
|
||||
);
|
||||
let commands: string[];
|
||||
if (typeof validate === "string") {
|
||||
commands = [validate];
|
||||
|
||||
@@ -77,10 +77,72 @@ export function getReadFileRangeError(request: ReadFileRequest): string | null {
|
||||
return `start_line must be less than or equal to end_line (received start_line: ${start_line}, end_line: ${end_line})`;
|
||||
}
|
||||
|
||||
function parseJsonLikeString(input: unknown): unknown {
|
||||
if (typeof input !== "string") {
|
||||
return input;
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) {
|
||||
return input;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed) as unknown;
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input != null && !Array.isArray(input);
|
||||
}
|
||||
|
||||
export function normalizeJsonLikeToolInput(
|
||||
input: unknown,
|
||||
keys: string[],
|
||||
): unknown {
|
||||
const parsed = parseJsonLikeString(input);
|
||||
if (!isRecord(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const normalized = { ...parsed };
|
||||
for (const key of keys) {
|
||||
if (!(key in normalized)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = parseJsonLikeString(normalized[key]);
|
||||
if (isRecord(value) && key in value) {
|
||||
normalized[key] = parseJsonLikeString(value[key]);
|
||||
} else {
|
||||
normalized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function normalizeJsonLikeRunCommandsInput(input: unknown): unknown {
|
||||
return normalizeJsonLikeToolInput(input, ["commands"]);
|
||||
}
|
||||
|
||||
export function normalizeJsonLikeReadFilesInput(input: unknown): unknown {
|
||||
return normalizeJsonLikeToolInput(input, ["files", "file_paths", "paths"]);
|
||||
}
|
||||
|
||||
export function normalizeJsonLikeSearchCodebaseInput(input: unknown): unknown {
|
||||
return normalizeJsonLikeToolInput(input, ["queries"]);
|
||||
}
|
||||
|
||||
export function normalizeRunCommandsInput(
|
||||
input: unknown,
|
||||
): Array<string | StructuredCommandInput> {
|
||||
const validate = validateWithZod(StructuredCommandsInputUnionSchema, input);
|
||||
const validate = validateWithZod(
|
||||
StructuredCommandsInputUnionSchema,
|
||||
normalizeJsonLikeRunCommandsInput(input),
|
||||
);
|
||||
|
||||
if (typeof validate === "string") {
|
||||
return [validate];
|
||||
|
||||
@@ -155,15 +155,11 @@ export const StructuredCommandsInputSchema = z.object({
|
||||
* Union schema for run_commands tool input. More flexible.
|
||||
*/
|
||||
export const StructuredCommandsInputUnionSchema = z.union([
|
||||
RunCommandsInputSchema,
|
||||
StructuredCommandsInputSchema,
|
||||
z.object({ commands: StructuredCommandEntrySchema }),
|
||||
z.array(StructuredCommandInputSchema),
|
||||
StructuredCommandInputSchema,
|
||||
z.object({ command: CommandInputSchema }),
|
||||
z.object({ cmd: CommandInputSchema }),
|
||||
z.array(z.string()),
|
||||
z.string(),
|
||||
RunCommandsInputUnionSchema,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user