Compare commits

...
Author SHA1 Message Date
Arafatkatze 531bf3d315 Remove stale free model fallback 2026-05-04 11:10:27 -07:00
6 changed files with 87 additions and 41 deletions
+23 -4
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"
import { getAllFeaturedModels, mapRecommendedModelsToFeaturedModels } from "./featured-models"
import { getAllFeaturedModels, mapRecommendedModelsToFeaturedModels, withFeaturedModelFallback } from "./featured-models"
describe("featured models", () => {
it("includes display names for all featured models", () => {
@@ -10,14 +10,33 @@ describe("featured models", () => {
}
})
it("fills free model metadata from fallback when upstream payload is sparse", () => {
it("does not add fallback free models when upstream free models are empty", () => {
const models = mapRecommendedModelsToFeaturedModels({
recommended: [],
free: [],
})
expect(models.free).toEqual([])
})
it("only falls back recommended models", () => {
const models = withFeaturedModelFallback({
recommended: [],
free: [],
})
expect(models.recommended.length).toBeGreaterThan(0)
expect(models.free).toEqual([])
})
it("adds the free label to upstream free models when tags are omitted", () => {
const models = mapRecommendedModelsToFeaturedModels({
recommended: [],
free: [{ id: "trinity-large-preview:free", name: "trinity-large-preview:free", description: "", tags: [] }],
})
expect(models.free[0]?.name).toBe("Arcee AI Trinity Large Preview")
expect(models.free[0]?.description).toBe("Arcee AI's advanced large preview model in the Trinity series")
expect(models.free[0]?.name).toBe("trinity-large-preview:free")
expect(models.free[0]?.description).toBe("")
expect(models.free[0]?.labels).toContain("FREE")
})
})
+1 -2
View File
@@ -86,6 +86,5 @@ export function mapRecommendedModelsToFeaturedModels(data: RecommendedModelsByTi
export function withFeaturedModelFallback(modelsByTier: FeaturedModelsByTier): FeaturedModelsByTier {
const recommended = modelsByTier.recommended.length > 0 ? modelsByTier.recommended : FEATURED_MODELS.recommended
const free = modelsByTier.free.length > 0 ? modelsByTier.free : FEATURED_MODELS.free
return { recommended, free }
return { recommended, free: modelsByTier.free }
}
@@ -1,12 +1,14 @@
import "should"
import { openRouterDefaultModelInfo } from "@shared/api"
import sinon from "sinon"
import { resetClineRecommendedModelsCacheForTests } from "@/core/controller/models/refreshClineRecommendedModels"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService } from "@/services/auth/AuthService"
import { ClineHandler } from "../cline"
describe("ClineHandler", () => {
afterEach(() => {
resetClineRecommendedModelsCacheForTests()
sinon.restore()
})
@@ -64,6 +66,49 @@ describe("ClineHandler", () => {
])
})
it("should not zero usage cost for removed free fallback models", async () => {
const handler = createHandler({})
const fakeClient = {
chat: {
completions: {
create: sinon.stub().resolves(
createAsyncIterable([
{
choices: [{}],
usage: {
prompt_tokens: 17,
completion_tokens: 9,
cost: 0.02,
},
},
]),
),
},
},
}
sinon.stub(handler as any, "ensureClient").resolves(fakeClient as any)
sinon.stub(handler, "getModel").returns({
id: "kwaipilot/kat-coder-pro",
info: openRouterDefaultModelInfo,
})
const chunks: any[] = []
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
chunks.push(chunk)
}
chunks.should.deepEqual([
{
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: 0,
inputTokens: 17,
outputTokens: 9,
totalCost: 0.02,
},
])
})
it("should read Anthropic-style cache creation and read tokens from usage chunks", async () => {
const handler = createHandler({})
const fakeClient = {
+2 -7
View File
@@ -9,7 +9,6 @@ import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService } from "@/services/auth/AuthService"
import { buildClineExtraHeaders } from "@/services/EnvUtils"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@/shared/cline/recommended-models"
import type { ClineStorageMessage } from "@/shared/messages/content"
import { fetch, getAxiosSettings } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
@@ -37,8 +36,6 @@ 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)))
function getCacheReadTokens(usage: any): number {
return usage?.prompt_tokens_details?.cached_tokens || usage?.cache_read_input_tokens || 0
}
@@ -68,14 +65,12 @@ export class ClineHandler implements ApiHandler {
try {
const models = await refreshClineRecommendedModels()
const freeModelIds = models.free.map((model) => normalizeModelId(model.id)).filter((modelId) => modelId.length > 0)
if (freeModelIds.length > 0) {
return new Set(freeModelIds)
}
return new Set(freeModelIds)
} catch (error) {
Logger.error("Error resolving Cline free model IDs from recommended models:", error)
}
return CLINE_FREE_MODEL_IDS
return new Set<string>()
}
private async ensureClient(): Promise<OpenAI> {
+1 -14
View File
@@ -40,18 +40,5 @@ export const CLINE_RECOMMENDED_MODELS_FALLBACK: ClineRecommendedModelsData = {
tags: ["NEW"],
},
],
free: [
{
id: "kwaipilot/kat-coder-pro",
name: "KwaiKAT Kat Coder Pro",
description: "KwaiKAT's most advanced agentic coding model in the KAT-Coder series",
tags: ["FREE"],
},
{
id: "arcee-ai/trinity-large-preview:free",
name: "Arcee AI Trinity Large Preview",
description: "Arcee AI's advanced large preview model in the Trinity series",
tags: ["FREE"],
},
],
free: [],
}
@@ -88,10 +88,6 @@ const RECOMMENDED_MODELS_FALLBACK: FeaturedModelCardEntry[] = CLINE_RECOMMENDED_
.map((model) => toFeaturedModelCardEntry(model, "RECOMMENDED"))
.filter((model): model is FeaturedModelCardEntry => model !== null)
const FREE_MODELS_FALLBACK: FeaturedModelCardEntry[] = CLINE_RECOMMENDED_MODELS_FALLBACK.free
.map((model) => toFeaturedModelCardEntry(model, "FREE"))
.filter((model): model is FeaturedModelCardEntry => model !== null)
const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMode, showProviderRouting, initialTab }) => {
const { handleModeFieldsChange, handleFieldChange } = useApiConfigurationHandlers()
const { apiConfiguration, favoritedModelIds, clineModels, refreshClineModels } = useExtensionState()
@@ -102,9 +98,7 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
const [clineRecommendedModels, setClineRecommendedModels] = useState<FeaturedModelCardEntry[]>([])
const [clineFreeModels, setClineFreeModels] = useState<FeaturedModelCardEntry[]>([])
const freeClineModelIds = useMemo(() => {
const freeModelIds =
clineFreeModels.length > 0 ? clineFreeModels.map((model) => model.id) : FREE_MODELS_FALLBACK.map((model) => model.id)
return [...new Set(freeModelIds)]
return [...new Set(clineFreeModels.map((model) => model.id))]
}, [clineFreeModels])
const freeClineModelIdSet = useMemo(
() => new Set(freeClineModelIds.map((modelId) => normalizeModelId(modelId))),
@@ -115,7 +109,8 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
() => (clineRecommendedModels.length > 0 ? clineRecommendedModels : RECOMMENDED_MODELS_FALLBACK),
[clineRecommendedModels],
)
const freeModels = useMemo(() => (clineFreeModels.length > 0 ? clineFreeModels : FREE_MODELS_FALLBACK), [clineFreeModels])
const freeModels = clineFreeModels
const hasFreeModels = freeModels.length > 0
const hasSuccessfulClineRecommendedModelsFetchRef = useRef(false)
const isFetchingClineRecommendedModelsRef = useRef(false)
const clineRecommendedModelsRetryTimeoutRef = useRef<number | null>(null)
@@ -179,10 +174,14 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
}, [clearClineRecommendedModelsRetryTimeout])
useEffect(() => {
if (initialTab) {
setActiveTab(initialTab)
if (initialTab === "free") {
setActiveTab(hasFreeModels ? "free" : "recommended")
return
}
}, [initialTab])
if (initialTab === "recommended") {
setActiveTab("recommended")
}
}, [hasFreeModels, initialTab])
useEffect(() => {
if (initialTab) {
@@ -400,9 +399,11 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
<Tab active={activeTab === "recommended"} onClick={() => setActiveTab("recommended")}>
Recommended
</Tab>
<Tab active={activeTab === "free"} onClick={() => setActiveTab("free")}>
Free
</Tab>
{hasFreeModels && (
<Tab active={activeTab === "free"} onClick={() => setActiveTab("free")}>
Free
</Tab>
)}
</TabsContainer>
{/* Model Cards */}