mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
fix onboarding models
This commit is contained in:
committed by
Dominic Cooney
parent
6dde25f2b0
commit
e19abb90b7
+31
-29
@@ -1,17 +1,13 @@
|
||||
import * as sdkCore from "@cline/core"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ClineEnv } from "@/config"
|
||||
import { getFeatureFlagsService } from "@/services/feature-flags"
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@/shared/cline/recommended-models"
|
||||
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
import { refreshClineRecommendedModels, resetClineRecommendedModelsCacheForTests } from "../refreshClineRecommendedModels"
|
||||
|
||||
// The HTTP fetch + normalization + offline fallback now lives in the SDK
|
||||
// The HTTP fetch + normalization + offline fallback lives in the SDK
|
||||
// (`@cline/core` `fetchClineRecommendedModels`). These tests cover the
|
||||
// extension-side wrapper: the feature-flag gate, delegation to the SDK, and the
|
||||
// in-memory cache / flag re-check. This suite is vitest-native (not mocha)
|
||||
// because it imports the ESM-only `@cline/core`; vitest aliases it to
|
||||
// src/test/cline-core-vitest-stub.ts, which we spy on here.
|
||||
// extension-side wrapper: delegation to the SDK and in-memory caching. There is
|
||||
// intentionally no feature-flag gate here; onboarding must not race against the
|
||||
// remote-config cache and accidentally keep the hardcoded fallback list.
|
||||
|
||||
describe("refreshClineRecommendedModels", () => {
|
||||
beforeEach(() => {
|
||||
@@ -28,20 +24,7 @@ describe("refreshClineRecommendedModels", () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("returns the hardcoded fallback list and skips the SDK fetch when the rollout flag is off", async () => {
|
||||
vi.spyOn(getFeatureFlagsService(), "getBooleanFlagEnabled").mockReturnValue(false)
|
||||
const sdkSpy = vi.spyOn(sdkCore, "fetchClineRecommendedModels")
|
||||
|
||||
const result = await refreshClineRecommendedModels()
|
||||
|
||||
expect(result).toEqual(CLINE_RECOMMENDED_MODELS_FALLBACK)
|
||||
expect(sdkSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("delegates to the SDK fetch when the rollout flag is on", async () => {
|
||||
vi.spyOn(getFeatureFlagsService(), "getBooleanFlagEnabled").mockImplementation(
|
||||
(flag) => flag === FeatureFlag.CLINE_RECOMMENDED_MODELS_UPSTREAM,
|
||||
)
|
||||
it("delegates to the SDK fetch", async () => {
|
||||
const sdkResult = {
|
||||
recommended: [{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", description: "Remote", tags: ["NEW"] }],
|
||||
free: [{ id: "z-ai/glm-5", name: "GLM 5", description: "Remote free", tags: [] }],
|
||||
@@ -54,18 +37,37 @@ describe("refreshClineRecommendedModels", () => {
|
||||
expect(result).toEqual(sdkResult)
|
||||
})
|
||||
|
||||
it("re-checks the rollout flag on each call (off after on returns the fallback)", async () => {
|
||||
const flagSpy = vi.spyOn(getFeatureFlagsService(), "getBooleanFlagEnabled")
|
||||
flagSpy.mockReturnValueOnce(true).mockReturnValueOnce(false)
|
||||
vi.spyOn(sdkCore, "fetchClineRecommendedModels").mockResolvedValue({
|
||||
it("uses the in-memory cache after a populated upstream result", async () => {
|
||||
const sdkResult = {
|
||||
recommended: [{ id: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro", description: "Remote", tags: ["NEW"] }],
|
||||
free: [],
|
||||
})
|
||||
}
|
||||
const sdkSpy = vi.spyOn(sdkCore, "fetchClineRecommendedModels").mockResolvedValue(sdkResult)
|
||||
|
||||
const firstResult = await refreshClineRecommendedModels()
|
||||
const secondResult = await refreshClineRecommendedModels()
|
||||
|
||||
expect(firstResult).not.toEqual(CLINE_RECOMMENDED_MODELS_FALLBACK)
|
||||
expect(secondResult).toEqual(CLINE_RECOMMENDED_MODELS_FALLBACK)
|
||||
expect(sdkSpy).toHaveBeenCalledTimes(1)
|
||||
expect(secondResult).toEqual(firstResult)
|
||||
})
|
||||
|
||||
it("does not cache the SDK fallback result", async () => {
|
||||
const sdkFallbackClone = structuredClone(sdkCore.FALLBACK_CLINE_RECOMMENDED_MODELS)
|
||||
const sdkSpy = vi
|
||||
.spyOn(sdkCore, "fetchClineRecommendedModels")
|
||||
.mockResolvedValueOnce(sdkFallbackClone)
|
||||
.mockResolvedValueOnce({
|
||||
recommended: [
|
||||
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", description: "Remote", tags: ["NEW"] },
|
||||
],
|
||||
free: [],
|
||||
})
|
||||
|
||||
const firstResult = await refreshClineRecommendedModels()
|
||||
const secondResult = await refreshClineRecommendedModels()
|
||||
|
||||
expect(sdkSpy).toHaveBeenCalledTimes(2)
|
||||
expect(firstResult).toEqual(sdkCore.FALLBACK_CLINE_RECOMMENDED_MODELS)
|
||||
expect(secondResult).not.toEqual(sdkCore.FALLBACK_CLINE_RECOMMENDED_MODELS)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { fetchClineRecommendedModels } from "@cline/core"
|
||||
import { FALLBACK_CLINE_RECOMMENDED_MODELS, fetchClineRecommendedModels } from "@cline/core"
|
||||
import { ClineEnv } from "@/config"
|
||||
import { featureFlagsService } from "@/services/feature-flags"
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@/shared/cline/recommended-models"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
|
||||
export interface ClineRecommendedModelData {
|
||||
id: string
|
||||
@@ -22,19 +19,7 @@ const RECOMMENDED_MODELS_CACHE_TTL_MS = 60 * 60 * 1000
|
||||
let pendingRefresh: Promise<ClineRecommendedModelsData> | null = null
|
||||
let inMemoryCache: { data: ClineRecommendedModelsData; timestamp: number } | null = null
|
||||
|
||||
function getHardcodedRecommendedModels(): ClineRecommendedModelsData {
|
||||
return CLINE_RECOMMENDED_MODELS_FALLBACK
|
||||
}
|
||||
|
||||
function useUpstreamRecommendedModels(): boolean {
|
||||
return featureFlagsService.getBooleanFlagEnabled(FeatureFlag.CLINE_RECOMMENDED_MODELS_UPSTREAM)
|
||||
}
|
||||
|
||||
export async function refreshClineRecommendedModels(): Promise<ClineRecommendedModelsData> {
|
||||
if (!useUpstreamRecommendedModels()) {
|
||||
return getHardcodedRecommendedModels()
|
||||
}
|
||||
|
||||
if (inMemoryCache && Date.now() - inMemoryCache.timestamp <= RECOMMENDED_MODELS_CACHE_TTL_MS) {
|
||||
return inMemoryCache.data
|
||||
}
|
||||
@@ -59,20 +44,25 @@ export function resetClineRecommendedModelsCacheForTests(): void {
|
||||
inMemoryCache = null
|
||||
}
|
||||
|
||||
function isFallbackRecommendedModels(data: ClineRecommendedModelsData): boolean {
|
||||
return JSON.stringify(data) === JSON.stringify(FALLBACK_CLINE_RECOMMENDED_MODELS)
|
||||
}
|
||||
|
||||
async function fetchAndCacheClineRecommendedModels(): Promise<ClineRecommendedModelsData> {
|
||||
// Delegate the actual HTTP fetch + response normalization + offline fallback
|
||||
// to the SDK so the CLI/JetBrains and the extension share one implementation.
|
||||
// We pass the proxy-aware fetch (per .clinerules/network.md) and the
|
||||
// extension's configured API base URL. On failure the SDK returns its own
|
||||
// fallback list (identical to CLINE_RECOMMENDED_MODELS_FALLBACK).
|
||||
// fallback list.
|
||||
const result = await fetchClineRecommendedModels({
|
||||
baseUrl: ClineEnv.config().apiBaseUrl,
|
||||
fetchImpl: fetch,
|
||||
})
|
||||
|
||||
// Only pin a populated, non-fallback result in memory for the full TTL; a
|
||||
// transient failure (SDK returns the fallback) should be retried next call.
|
||||
if ((result.recommended.length > 0 || result.free.length > 0) && result !== CLINE_RECOMMENDED_MODELS_FALLBACK) {
|
||||
// transient failure (SDK returns a clone of its fallback) should be retried
|
||||
// next call.
|
||||
if ((result.recommended.length > 0 || result.free.length > 0) && !isFallbackRecommendedModels(result)) {
|
||||
inMemoryCache = { data: result, timestamp: Date.now() }
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -187,6 +187,25 @@ export interface ClineRecommendedModelsData {
|
||||
free: ClineRecommendedModel[]
|
||||
}
|
||||
|
||||
export const FALLBACK_CLINE_RECOMMENDED_MODELS: ClineRecommendedModelsData = {
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
description: "Strong coding and agent performance",
|
||||
tags: ["NEW"],
|
||||
},
|
||||
],
|
||||
free: [
|
||||
{
|
||||
id: "z-ai/glm-5",
|
||||
name: "GLM 5",
|
||||
description: "Remote free",
|
||||
tags: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export async function fetchClineRecommendedModels(_options?: {
|
||||
baseUrl?: string
|
||||
fetchImpl?: typeof fetch
|
||||
|
||||
Reference in New Issue
Block a user