fix: make provider-lock catalog check case-insensitive

The matcher compares selector ids case-insensitively, but the lock's
bundled-catalog lookup was exact-case: Anthropic/Claude-Opus-5 exact-
matched OpenRouter's flat id while getBundledModel("Anthropic", ...)
missed, silently re-enabling the aggregator shadow the lock exists to
prevent. Scan the named provider's bundled ids case-insensitively.
This commit is contained in:
can1357
2026-08-19 01:15:14 +02:00
parent a4a54a0ff5
commit edc4f9ecb4
2 changed files with 15 additions and 5 deletions
@@ -22,7 +22,7 @@ import { modelMatchesHost } from "@oh-my-pi/pi-catalog/hosts";
import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
import { stripThinkingVariantToken } from "@oh-my-pi/pi-catalog/identity/family";
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
import { type GeneratedProvider, getBundledModel, modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
import { type GeneratedProvider, getBundledModels, modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
import { DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models";
import { resolveBareVariantAlias, resolveVariantAlias } from "@oh-my-pi/pi-catalog/variant-collapse";
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
@@ -619,12 +619,15 @@ function isProviderLockedCrossMatch(pattern: string, matchedModel: Model<Api>):
if (slashIdx <= 0) {
return false;
}
const provider = pattern.slice(0, slashIdx);
const modelId = pattern.slice(slashIdx + 1);
if (matchedModel.provider.toLowerCase() === provider.toLowerCase()) {
const provider = pattern.slice(0, slashIdx).toLowerCase();
const modelId = pattern.slice(slashIdx + 1).toLowerCase();
if (matchedModel.provider.toLowerCase() === provider) {
return false;
}
return getBundledModel(provider as GeneratedProvider, modelId) !== undefined;
// Case-insensitive on both halves: the surrounding matcher lowercases the
// selector before comparing ids, so the lock must not evaporate on case
// variance (catalog provider keys are lowercase; model ids may not be).
return getBundledModels(provider as GeneratedProvider).some(m => m.id.toLowerCase() === modelId);
}
/**
@@ -704,6 +704,13 @@ describe("parseModelPattern", () => {
expect(result.model).toBeUndefined();
});
test("the provider lock is case-insensitive (Anthropic/Claude-Opus-5 still fails closed)", () => {
// The matcher compares ids case-insensitively, so the lock must too —
// otherwise case variance silently re-enables the aggregator shadow.
const result = parseModelPattern("Anthropic/Claude-Opus-5", [openRouterOpus5]);
expect(result.model).toBeUndefined();
});
test("anthropic/claude-opus-5 resolves to the anthropic provider when it is available", () => {
const result = parseModelPattern("anthropic/claude-opus-5", [anthropicOpus5, openRouterOpus5]);
expect(result.model?.provider).toBe("anthropic");