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

  1. Tighten one family's selector (anchor/word-boundary the pattern) so only one matches.
  2. Give one family a higher rank/priority so the tie breaks deterministically.
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a71c8c17f3f7682f. Report an issue: GitHub.