From 7b2cc8c0f222b17618766bc553bdff159e817c4e Mon Sep 17 00:00:00 2001 From: ggdayup Date: Thu, 13 Aug 2026 11:40:32 +0800 Subject: [PATCH] fix(sse): apply free-tier filter to auto/best-free on chat path (#10199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit classifyAutoModel in autoRouting.ts returned only {variant:"cheap"} for auto/best-free, without the spec.tier="free" that builtinCatalog.ts hardcodes. chat.ts routes via resolveAutoRoutingState (autoRouting.ts), not createBuiltinAutoCombo (chatHelpers.ts), so the tier filter was skipped entirely and auto/best-free behaved as plain auto/cheap — paid backends (e.g. antigravity/gemini-3.6-flash-high) could be selected from the full pool. Mirrors the hardcoded spec from builtinCatalog.ts:120 in classifyAutoModel so both paths apply the free-tier candidate filter consistently. TDD: tests/unit/auto-best-free-tier-filter.test.ts (RED → GREEN). --- src/sse/handlers/autoRouting.ts | 7 +++- tests/unit/auto-best-free-tier-filter.test.ts | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/unit/auto-best-free-tier-filter.test.ts diff --git a/src/sse/handlers/autoRouting.ts b/src/sse/handlers/autoRouting.ts index f898f1094d..8920c71d33 100644 --- a/src/sse/handlers/autoRouting.ts +++ b/src/sse/handlers/autoRouting.ts @@ -32,7 +32,12 @@ function classifyAutoModel( const recognizedBuiltInAuto = model === "auto" || Object.prototype.hasOwnProperty.call(AUTO_TEMPLATE_VARIANTS, model); if (Object.prototype.hasOwnProperty.call(AUTO_TEMPLATE_VARIANTS, model)) { - return { variant: AUTO_TEMPLATE_VARIANTS[model], recognizedBuiltInAuto: true }; + // auto/best-free must carry spec.tier="free" so virtualFactory applies the + // free-tier candidate filter (excludes paid backends). Mirrors the + // hardcoded spec in builtinCatalog.ts:createBuiltinAutoCombo. Without this, + // chat.ts routes auto/best-free as plain auto/cheap (no tier filter). + const spec = model === "auto/best-free" ? { tier: "free" as const } : undefined; + return { variant: AUTO_TEMPLATE_VARIANTS[model], spec, recognizedBuiltInAuto: true }; } if (!model.startsWith("auto/")) return { recognizedBuiltInAuto }; diff --git a/tests/unit/auto-best-free-tier-filter.test.ts b/tests/unit/auto-best-free-tier-filter.test.ts new file mode 100644 index 0000000000..e7a1ed417d --- /dev/null +++ b/tests/unit/auto-best-free-tier-filter.test.ts @@ -0,0 +1,40 @@ +/** + * Regression: `auto/best-free` must carry `spec.tier = "free"` so the + * virtualFactory candidate filter (buildAutoCandidateFilter) excludes paid + * backends from the pool. + * + * Root cause: `classifyAutoModel` in `src/sse/handlers/autoRouting.ts` returns + * only `{ variant: "cheap" }` for `auto/best-free` (via AUTO_TEMPLATE_VARIANTS), + * WITHOUT setting `spec.tier = "free"`. The sibling path `createBuiltinAutoCombo` + * in `open-sse/services/autoCombo/builtinCatalog.ts` has a hardcoded special case + * `modelStr === "auto/best-free" ? { tier: "free" as const } : undefined`, but + * `chat.ts` routes through `resolveAutoRoutingState` → `createVirtualAutoCombo` + * (autoRouting.ts), NOT through `createBuiltinAutoCombo`. So the chat path + * skipped the tier filter entirely and `auto/best-free` behaved as plain + * `auto/cheap`, allowing paid models (e.g. antigravity/gemini-3.6-flash-high) + * to be selected from the full pool. + * + * classifyAutoModel() is module-private, so this exercises it through the public + * resolveAutoRoutingState() — same pattern as auto-family-classification-8866. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { resolveAutoRoutingState } from "../../src/sse/handlers/autoRouting.ts"; + +test("auto/best-free carries spec.tier='free' so the candidate filter excludes paid backends", async () => { + const state = await resolveAutoRoutingState("auto/best-free"); + assert.equal(state.recognizedBuiltInAuto, true); + assert.equal(state.variant, "cheap"); + assert.equal( + state.spec?.tier, + "free", + "auto/best-free must carry spec.tier='free' to trigger the free-tier candidate filter in virtualFactory" + ); +}); + +test("auto/best-coding does NOT carry a free tier spec (only auto/best-free is free-tier)", async () => { + const state = await resolveAutoRoutingState("auto/best-coding"); + assert.equal(state.recognizedBuiltInAuto, true); + assert.equal(state.variant, "coding"); + assert.notEqual(state.spec?.tier, "free"); +});