fix(sse): apply free-tier filter to auto/best-free on chat path (#10199)

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).
This commit is contained in:
ggdayup
2026-08-13 11:40:32 +08:00
committed by GitHub
parent 10c622afa6
commit 7b2cc8c0f2
2 changed files with 46 additions and 1 deletions

View File

@@ -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 };

View File

@@ -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");
});