JuliusBrussee/caveman · error · CatalogError

: duplicate identity

Error message

{label}: duplicate identity {'/'.join(identity)}

What it means

validate_catalog.py derives an identity triple for every row of current.yaml via validate_row and rejects the catalog if two rows share the same identity, since the identity uniquely keys a provider/model/price entry. Duplicate identities would make snapshot matching ambiguous downstream (pricing_identity lookups), so the validator fails fast naming the offending row and the colliding identity joined with '/'.

Solutions

  1. Locate the reported row in current.yaml and the earlier row with the same identity; delete or correct the duplicate
  2. If both rows are intentional variants, differentiate the identity fields (e.g. distinct model name or unit) so they no longer collide
  3. Re-run the sync tool so it deduplicates instead of re-adding the row
  4. Re-run validate_catalog to confirm the identity set is unique

Example fix

# before (current.yaml)
- provider: openai
  model: gpt-4o
  monthly_input: 2.50
- provider: openai
  model: gpt-4o
  monthly_input: 2.75
# after (deduplicated)
- provider: openai
  model: gpt-4o
  monthly_input: 2.75
Defensive patterns

Strategy: validation

Validate before calling

def check_unique_identities(rows):
    seen = set()
    for i, row in enumerate(rows):
        identity = (row["provider"], row["model"], row["unit"])
        if identity in seen:
            raise ValueError(f"row {i+1}: duplicate identity {'/'.join(identity)}")
        seen.add(identity)

Prevention

When it happens

Trigger: Running validate_catalog when two rows in current.yaml produce the identical (provider, model, unit or equivalent) identity tuple from validate_row; the second occurrence raises with label 'current.yaml row N' and the joined identity.

Common situations: A copy-paste of a model row with only the price changed; an automated sync re-inserting a row that already exists; merging two branches that both added the same model entry; a typo in the field that would otherwise distinguish rows (e.g. same provider/model spelling).

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/364aca4d39ed2afc. Report an issue: GitHub.

Appendix: 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 PRICING

View on GitHub (pinned to 3ee70a1026)