JuliusBrussee/caveman · error · CatalogError
: currency must be USD
Error message
{label}: currency must be USD What it means
A row's `currency` is not the literal string "USD". All prices in this catalog are denominated in USD — cross-currency rows would corrupt the no-fake-savings cost math — so the validator pins the currency exactly. Thrown from validate_row.
Solutions
- Set `currency: USD` exactly (uppercase)
- If your source price is in another currency, convert it to USD and cite the USD figure from the provider's pricing page (usually switchable to USD)
- Remove any stray currency field value like null or an empty string and write USD
Example fix
# before currency: usd # after currency: USD
Defensive patterns
Strategy: validation
Validate before calling
def currency_ok(row: dict) -> bool:
return row.get("currency") == "USD" Type guard
def is_usd(value: object) -> bool:
return value == "USD" Try / catch
try:
validate_catalog()
except CatalogError as e:
if "currency must be USD" in str(e):
normalize_currency(str(e)) Prevention
- Convert any non-USD source price to USD before adding a row
- Always write the literal uppercase `USD`
- Assert currency == USD in any script that emits rows
When it happens
Trigger: A row has `currency: usd` (lowercase), `currency: EUR`, `currency: null`, or omits any currency normalization, e.g. after copying a price from a provider page quoting in another currency.
Common situations: Copying a European provider/VAT-inclusive EUR price; a lowercase typo; a sync script carrying through a currency field from a third-party models dataset; new row authored from a non-US pricing page.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- catalog
- : must be a safe non-empty identifier
- : catalog must be a non-empty list
- catalog is empty
- duplicate provider/model/region row
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b160282fa14ce435.
Report an issue: GitHub.
Appendix: source
Thrown at shared/provider-catalog/validate_catalog.py:131
return parsed.astimezone(timezone.utc)
def validate_row(row: dict[str, Any], label: str, now: datetime) -> tuple[str, str, str]:
unknown = set(row) - TOP_LEVEL_KEYS
missing = REQUIRED_KEYS - set(row)
if unknown:
raise CatalogError(f"{label}: unknown field(s): {', '.join(sorted(unknown))}")
if missing:
raise CatalogError(f"{label}: missing field(s): {', '.join(sorted(missing))}")
identity: list[str] = []
for field in ("provider", "model", "region"):
value = row[field]
if not isinstance(value, str) or not IDENTIFIER.fullmatch(value):
raise CatalogError(f"{label}: {field} must be a safe non-empty identifier")
identity.append(value)
if row["currency"] != "USD":
raise CatalogError(f"{label}: currency must be USD")
pricing = row["pricing"]
if not isinstance(pricing, dict):
raise CatalogError(f"{label}: pricing must be an object")
pricing_unknown = set(pricing) - PRICING_KEYS
if pricing_unknown:
raise CatalogError(f"{label}: unknown pricing field(s): {', '.join(sorted(pricing_unknown))}")
for required in ("input_per_million", "output_per_million"):
if required not in pricing:
raise CatalogError(f"{label}: pricing.{required} is required")
for field, value in pricing.items():
if value is None or field == "long_context_threshold_inclusive":
if field == "long_context_threshold_inclusive" and value is not None and not isinstance(value, bool):
raise CatalogError(f"{label}: pricing.{field} must be boolean or null")
continue
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise CatalogError(f"{label}: pricing.{field} must be numeric or null")
if not math.isfinite(value) or value < 0:View on GitHub (pinned to 3ee70a1026)