fix(ui): only check classifier/embedding models the config actually submits

missingReferencedModels read complexityRouterConfig.classifier_llm_config and embeddingModel unconditionally, but buildComplexityRouterConfig only includes classifier_llm_config when classifierType is "llm" and embedding_model when semanticMatchingEnabled is on. A dormant selection left over from a toggle no longer in effect (embeddingModel still set after turning semantic matching off, or a classifier_llm_config seeded with model: "" before a caller picks one) was being checked and reported as a missing model that would never actually be submitted, wrongly disabling the button.

Gate both fields on the same conditions buildComplexityRouterConfig itself uses. Also hardened getRequiredModels to filter out empty-string models, not just null/undefined, so a not-yet-chosen classifier model can't be misread as a real reference even when its field is legitimately included.
This commit is contained in:
Tin Chi Lo 2026-08-03 20:18:08 -07:00
parent 13c99829c7
commit e4caa1d998
3 changed files with 22 additions and 4 deletions

View file

@ -201,12 +201,16 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
// Checks the config actually being built, not which preset (if any) it came from: a model that
// was available when it entered a tier, whether via a preset or picked by hand, can have gone
// missing since (the caller's access narrowed, or a background refetch never caught it).
// missing since (the caller's access narrowed, or a background refetch never caught it). Only
// includes classifier_llm_config/embedding_model when buildComplexityRouterConfig would actually
// emit them (classifierType === "llm", semanticMatchingEnabled) - otherwise a dormant selection
// left over from a toggle no longer in effect would block submit for a model that never ships.
const missingReferencedModels = getMissingModels(
{
tiers: complexityRouterConfig.tiers,
classifier_llm_config: complexityRouterConfig.classifier_llm_config,
embedding_model: embeddingModel,
classifier_llm_config:
complexityRouterConfig.classifier_type === "llm" ? complexityRouterConfig.classifier_llm_config : undefined,
embedding_model: semanticMatchingEnabled ? embeddingModel : undefined,
},
availableModelSet,
);

View file

@ -4,6 +4,7 @@ import {
getPresetByKey,
getRequiredModelsInPreset,
getMissingModelsInPreset,
getRequiredModels,
} from "./autorouter_presets";
describe("autorouter_presets", () => {
@ -49,4 +50,15 @@ describe("autorouter_presets", () => {
);
expect(getMissingModelsInPreset(preset, new Set(required))).toEqual([]);
});
// A classifier_llm_config placeholder is seeded with model: "" before a caller picks one; an
// empty string is not a real model reference and must not be reported as an unavailable model.
it("does not treat an empty-string classifier or embedding model as a required model", () => {
const required = getRequiredModels({
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_llm_config: { model: "" },
embedding_model: "",
});
expect(required).toEqual(new Set(["gpt-5-nano"]));
});
});

View file

@ -30,7 +30,9 @@ export const getRequiredModels = (
): Set<string> => {
const { tiers, classifier_llm_config: classifier, embedding_model: embedding } = config;
const models = [...tiers.SIMPLE, ...tiers.MEDIUM, ...tiers.COMPLEX, ...tiers.REASONING, classifier?.model, embedding];
return new Set(models.filter((model): model is string => model != null));
// Boolean(), not != null: an empty-string placeholder (e.g. classifier_llm_config seeded before a
// model is chosen) is never a real model reference either.
return new Set(models.filter((model): model is string => Boolean(model)));
};
export const getMissingModels = (