can1357/oh-my-pi · critical · AmbiguousIdentityError
ambiguous family for `${model}`: `${tied[0]}` and `${tied[1]
Error message
ambiguous family for `${model}`: `${tied[0]}` and `${tied[1]}` tie What it means
AmbiguousIdentityError from classifyFamily: two family selectors in the taxonomy both match a model name with the same (winning) rank, so family classification cannot pick one. In lenient mode it returns undefined instead of throwing; strict mode surfaces the two tied family ids in the message.
Source
Thrown at packages/catalog/src/compat/taxonomy.ts:106
return count;
}
function classifyFamily(cls: CompiledClass, bare: string, model: string, lenient: boolean): string | undefined {
let winner: { rank: readonly [number, number]; id: string } | undefined;
let tied: readonly [string, string] | undefined;
for (const family of cls.families) {
if (!globMatch(family.glob, bare)) continue;
const rank = [family.priority, nonWildcardBytes(family.glob)] as const;
if (winner && winner.rank[0] === rank[0] && winner.rank[1] === rank[1] && winner.id !== family.id) {
tied = [winner.id, family.id];
} else if (!winner || winner.rank[0] < rank[0] || (winner.rank[0] === rank[0] && winner.rank[1] < rank[1])) {
winner = { rank, id: family.id };
tied = undefined;
}
}
if (tied) {
if (lenient) return undefined;
throw new AmbiguousIdentityError(model, tied[0], tied[1], "family");
}
return winner?.id;
}
function extractRevision(cls: CompiledClass, bare: string): string | undefined {
if (cls.skipBare.includes(bare)) return undefined;
for (const rule of cls.revisionPrefixes) {
let tail: string | undefined;
if (rule.anywhere) {
const start = bare.indexOf(rule.prefix);
if (start !== -1) tail = bare.slice(start + rule.prefix.length);
} else if (bare.startsWith(rule.prefix)) {
tail = bare.slice(rule.prefix.length);
}
if (tail === undefined) continue;
const digit = tail.search(/[0-9]/);
if (digit === -1) return undefined;
const revision = parseRevisionPrefix(tail.slice(digit));View on GitHub (pinned to 9690622007)
Solutions
- Tighten one family's selector (anchor/word-boundary the pattern) so only one matches.
- Give one family a higher rank/priority so the tie breaks deterministically.
- Run `bun run gen:compat` after editing the taxonomy KDL and re-test classifyModel on the colliding id.
Example fix
// before: two families both match "gpt-5-mini"
family "gpt" { match "gpt" }
// after: anchor so only the specific family matches
family "gpt-5-mini" { match "gpt-5-mini" } Defensive patterns
Strategy: try-catch
Validate before calling
null
Type guard
null
Try / catch
try {
const facts = classifyModel(id);
} catch (err) {
if (err instanceof AmbiguousIdentityError && err.kind === "family") {
// err names the two tied families; fall back to unknown-family behavior
return defaultFacts(id);
}
throw err;
} Prevention
- Anchor/word-boundary family patterns so broad and specific families don't both match.
- Give specific families higher rank than wildcards.
- Add classifyModel assertions in tests for ids at collision risk.
When it happens
Trigger: classifyModel/classifyFamily on a bare model string where two family regex/prefix patterns in taxonomy/*.kdl both match at equal priority — e.g. overlapping families like "gpt" and "gpt-5" both matching with the same rank.
Common situations: Adding a new family KDL rule whose pattern overlaps an existing family without distinguishing specificity; upstream model names colliding with broad family wildcards.
Related errors
- ambiguous class for `${lower}`: `${tied[0]}` and `${tied[1]}
- ambiguous overlap for `${provider}/${model}` on axis `${axis
- invalid compiled revision term in ${compiled.source}
- Model ${spec.provider}/${spec.id} resolved to an empty think
- missing reviewed collapse table for google-gemini-cli
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/a71c8c17f3f7682f.
Report an issue: GitHub.