JuliusBrussee/caveman · error · CatalogError
{label}: duplicate identity {'/'.join(identity)}
Error message
{label}: duplicate identity {'/'.join(identity)} What it means
Raised by validate_catalog() when two rows in current.yaml share the same (provider, model, region) identity tuple — the three-part key validate_row() returns and the loop accumulates in `seen`. The catalog is keyed by that tuple; a duplicate makes lookups ambiguous (which row answers a price query), so validation fails fast with the duplicated identity in the message.
Source
Thrown at shared/provider-catalog/validate_catalog.py:220
identity["price_affecting_capabilities"] = {
key: capabilities[key] for key in PRICE_AFFECTING_CAPABILITY_KEYS if key in capabilities
}
return identity
def validate_catalog(now: datetime | None = None) -> None:
check_time = now or datetime.now(timezone.utc)
current_path = CATALOG_DIR / "current.yaml"
current = load_yaml(current_path)
check_review_markers(current_path.read_text(encoding="utf-8"), "current.yaml")
snapshots: dict[str, list[dict[str, Any]]] = {}
seen: set[tuple[str, str, str]] = set()
for index, row in enumerate(current):
label = f"current.yaml row {index + 1}"
identity = validate_row(row, label, check_time)
if identity in seen:
raise CatalogError(f"{label}: duplicate identity {'/'.join(identity)}")
seen.add(identity)
verified = parsed_time(row["verified_at"], label).date().isoformat()
snapshot = snapshots.setdefault(
verified,
load_yaml(CATALOG_DIR / f"{verified}.yaml"),
)
identity = pricing_identity(row)
matched = next(
(snap_row for snap_row in snapshot if pricing_identity(snap_row) == identity),
None,
)
if matched is None:
raise CatalogError(
f"{label}: pricing changed without a new verified_at snapshot version (checked catalog/{verified}.yaml)"
)
# sources is excluded from pricing_identity so a capability citation can
# be added without minting a new price-dated snapshot, but a PRICINGView on GitHub (pinned to 27d5a3981a)
Solutions
- Search current.yaml for the duplicated provider/model/region tuple named in the message and delete or differentiate the redundant row.
- If you meant to update pricing, edit the EXISTING row (bumping verified_at and minting a snapshot) instead of adding a second entry.
- If regions genuinely differ (e.g. 'us' vs 'global'), fix the region field on the row you intended to change.
Example fix
# before — two identical identities
- {provider: openai, model: gpt-4o, region: us, ...}
- {provider: openai, model: gpt-4o, region: us, ...} # duplicate
# after — one row, pricing edited in place
- {provider: openai, model: gpt-4o, region: us, pricing: {...updated...}, verified_at: "2026-08-15T00:00:00Z"} Defensive patterns
Strategy: validation
Validate before calling
def duplicate_identities(rows: list[dict]) -> list[tuple]:
seen, dupes = set(), []
for row in rows:
key = (row.get("provider"), row.get("model"), row.get("region"))
if key in seen:
dupes.append(key)
seen.add(key)
return dupes Try / catch
try:
validate_catalog()
except CatalogError as e:
if "duplicate identity" in str(e):
dedupe_by_identity("current.yaml") # keep/merge into the verified row
raise Prevention
- Before adding a row, grep current.yaml for the provider/model/region tuple.
- Make sync scripts upsert by identity instead of appending.
- Resolve merge conflicts by picking one side per identity, never both.
When it happens
Trigger: Adding a row for a provider/model/region combination that already exists (e.g. a second 'openai/gpt-4o/us' entry), a sync script appending rather than replacing rows, or a copy-paste edit that forgot to change region or model.
Common situations: Manual catalog edits copying a neighboring row as a template; automated sync tools that append updates instead of upserting; merge conflicts resolved by keeping both sides' rows.
Related errors
- {label}: pricing changed without a new verified_at snapshot
- duplicate provider/model/region row %s/%s@%s
- ${where} hard chain edge ${edge.from}->${edge.to} is not a h
- ${where} is diagnosis-only but carries a change set, eval pa
- ${where} change set or eval pack is bound to another case
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/364aca4d39ed2afc.
Report an issue: GitHub.