GenAI Prices
repository·main·Indexed 43 days ago
https://github.com/pydantic/genai-pricesTools and data for estimating the cost of LLM inference APIs. Supports Python, JS/TS, and Go, providing capabilities to calculate prices using embedded or remote JSON data, extract usage from provider responses, and define custom pricing for new models.
What's inside GenAI Prices
- GenAI Prices is a tool designed to calculate the costs of calling various LLM (Large Language Model) inference APIs. It provides advanced logic for matching model and provider IDs, supports historical pricing changes, handles variable daily pricing (e.g., off-peak pricing for Deepseek), and supports tiered pricing for models with large contexts (e.g., Gemini).
How usage value reads and errors work
mainWhen calling
calc_price, the system performs demand-driven usage reads. It does not normalize all data upfront; instead, it only checks for contradictions when a specific value is needed for pricing.Read Behavior
- Reported values: Returned directly without checking for contradictions in other fields.
- Missing reported values (Zero): If a requested value wasn't reported and no positive related values imply it should be non-zero, it returns
0without storing it as reported usage. - Missing reported values (Error): If a requested value wasn't reported, but positive reported values for related units imply that an ancestor or overlap was omitted, a user-facing missing-usage error is raised.
Error Handling: Negative Leaf Values
If the decomposition results in a negative value (
leaf(U) < 0), it means the reported usage is contradictory (e.g., a more-specific count is larger than its ancestor). In this case, the price calculation raises a user-facing error rather than returning a nonsensical cost.Note: Contradictory usage is allowed to exist in the raw data. Errors are only raised if the contradiction affects the specific priced buckets being requested.
Understand v3 provider contract evolution
mainThe v3 provider member is additive. It begins with the v2 provider contract and evolves by adding optional members and structural variants.
Behavioral Rules for Extensions:
- Ignorable Extensions: An added field is ignorable only if the recognized form remains correct without it.
- Granular Skipping: If a client encounters an unsupported structural variant (like an unknown extractor or match variant), it skips only the smallest containing capability (e.g., that specific extractor) rather than rejecting the entire provider, model, or price.
- Distinguishable Variants: To prevent misinterpretation, any extension that changes behavior must use a distinguishable variant shape or discriminator so older clients can safely skip it.
Understand the Phase 1 Static Unit Registry architecture
mainPhase 1 of the
genai-pricesregistry introduces a static, immutableUnitRegistrythat is bundled with the installed package. This registry defines the units (e.g., tokens, requests, web searches) used for pricing calculations.Key architectural principles:
- Immutability: The
UnitRegistryis constructed once from bundled data (data_units.pyordataUnits.ts) and remains static for the lifetime of the package. Provider updates or custom snapshots do not mutate this registry. - Separation of Concerns: Unit definitions (the 'what') are separated from provider data (the 'how much').
- Dimension-Driven Pricing: Units are organized by dimensions (e.g.,
modality,direction,token_type). Dimensions define how units overlap, how they contain one another (ancestors), and how costs are aggregated. - Static vs. Dynamic: While provider prices can be updated via auto-updaters, the underlying unit vocabulary (the keys and dimensions) is frozen for the duration of the Phase 1 release.
- Immutability: The
Handle fractional usage values in `Usage`
mainThe
Usageobject supportsint,float, andDecimalfor all reportable units (e.g.,input_tokens,audio_seconds).- Precision: If any operand in an arithmetic operation is a
Decimal, the result will be aDecimal. Otherwise, it follows standard Python float/int rules. - Audio Usage: For Groq's Whisper models, report duration using
audio_secondsorinput_audio_seconds. Note that Groq applies a 10-second minimum; a zero or missing duration results in zero cost. - Serialization:
Decimal-bearing usage is not serializable by Python's standardjsonencoder without a custom converter.
from decimal import Decimal from genai_prices import Usage # Float arithmetic duration = Usage(audio_seconds=0.1) + Usage(audio_seconds=0.2) assert duration.audio_seconds == 0.3 # Decimal arithmetic for precision exact_duration = Usage(audio_seconds=Decimal('0.1')) + Usage(audio_seconds=0.2) assert exact_duration.audio_seconds == Decimal('0.3')- Precision: If any operand in an arithmetic operation is a
How unit dimensions and hierarchies work
mainDimensions define how units relate to one another through ancestors and joins:
- Ancestors (Hierarchy): A unit's dimensions define its containment. If unit A is an ancestor of unit B, A represents a more general category. For example, a specific
token_type: cache_writeunit is a descendant of a more generaltokensancestor. - Joins (Intersections): When two units have compatible but non-overlapping dimensions, their union defines a 'join'.
- Catch-all Behavior: If a specific descendant (e.g.,
output_image_tokens) is not explicitly priced, the registry uses the nearest priced ancestor (e.g.,output_mtok) as a catch-all.
Important Rule: Every priced usage value must land in exactly one bucket. Decomposition logic ensures that usage is not double-counted, dropped, or arbitrarily assigned when units overlap.
- Ancestors (Hierarchy): A unit's dimensions define its containment. If unit A is an ancestor of unit B, A represents a more general category. For example, a specific
Understand Go unit identifier transformations
mainWhen using Go, unit identifiers undergo a deterministic transformation applied by the publisher. This ensures consistency with the
package_go_dataformat.Transformation Rules:
- Parts are underscore-separated.
- Underscore-separated parts are Title-Cased.
- Parts starting with a digit are UPPERCASED.
- Empty parts become
_.
Handle overlapping modalities with Join Coverage (Mobius inversion)
mainSome modalities overlap (e.g., 'cached audio' is both 'cached' and 'audio'). To prevent double-counting, the system uses a decomposition algorithm (Mobius inversion) to ensure every token lands in exactly one bucket.
Example: Audio with Cache Overlap If a model defines:
input_tokens(catch-all)cache_read_tokens(cached text/general)input_audio_tokens(audio)cache_audio_read_tokens(the overlap: cached audio)
Given usage:
{input_tokens: 1000, cache_read_tokens: 200, input_audio_tokens: 300, cache_audio_read_tokens: 50}The system calculates the unique buckets as:
cache_audio_read_tokens: 50cache_read_tokens: 200 - 50 = 150input_audio_tokens: 300 - 50 = 250input_tokens: 1000 - 200 - 300 + 50 = 550
Critical Note: If
cache_audio_read_tokensis omitted while bothcache_read_tokensandinput_audio_tokensare positive, the runtime will raise an error rather than assuming the overlap is zero.Understand the unit decomposition and catch-all pricing model
mainThe pricing system uses a parent-child hierarchy to handle multimodal usage. A 'catch-all' unit (the ancestor) represents the default rate for any unclassified or remainder tokens. Specific modalities (the children) can have different rates.
Key Rules:
- Catch-all Ancestors: If a user provides usage without a specific breakdown (e.g.,
{input_tokens: 1000}), the tokens are priced using the ancestor's rate (e.g.,input_mtok). - Remainder Calculation: When specific child units are provided, the ancestor's effective usage is the remainder after subtracting the children. For example, if
input_tokensis 1000 andinput_audio_tokensis 300, the system calculates 700 tokens at the baseinput_mtokrate. - Mandatory Ancestors: If a model defines a priced ancestor (like
input_tokens), that ancestor must be accounted for in the usage. If usage is reported for children but the ancestor is missing, the runtime will raise an error. Missing descendants are treated as zero, but missing ancestors are not inferred.
- Catch-all Ancestors: If a user provides usage without a specific breakdown (e.g.,
How Go decodes and manages pricing data with the immutable calculator
mainThe Go implementation uses a 'wrapped' JSON structure to construct a new, immutable
Calculator. This approach ensures that updates are atomic: a caller-managed updater fetches data and only replaces the existing calculator reference if the new construction succeeds.Key behaviors:
- Immutability: Existing calculators and package-global bundled state are never modified. Every decode operation produces a new instance.
- Error Handling: Validation failures wrap
ErrInvalidData. - Tolerance: The decoder is designed to be forward-compatible. It ignores unknown object members and unsupported structural variants (skipping only the unsupported capability) while recording contextual compatibility warnings.
- Warning Propagation: Compatibility warnings are stored in an immutable slice within the
Calculatorand are appended to theWarningsfield ofPriceCalculationorExtractedUsageresults during runtime usage.
// Example of the pattern for updating a calculator in Go // The caller is responsible for managing the reference and ensuring atomicity. newCalc, err := NewCalculatorFromJSON(data) if err != nil { // Handle error (e.g., ErrInvalidData) return err } // Replace the old reference only after successful construction myCalculator = newCalcUse fractional usage values for non-token units
mainThe package supports finite non-negative numbers, including fractions, for usage units like duration (e.g.,
audio_seconds).For Groq's Whisper models, report transcription duration as
audio_secondsorinput_audio_seconds. Note that these models apply a 10-second minimum; a missing or zero duration will result in zero cost.const usage = { audio_seconds: 0.25 } JSON.stringify(usage)Understand the v3 Data Contract and Runtime Defaults
mainThe project has transitioned to a v3 data contract where providers and their dependent unit definitions are bundled together in a single wrapped payload.
Key characteristics of the v3 contract:
- Bundled Units: Unit definitions are kept with the provider fields that depend on them to ensure consistency.
- Append-only Evolution: The registry evolves in an append-only manner. New data must be compatible with the existing stable core.
- Default Remote URLs: All runtimes default to the v3 data endpoint:
- Python: Default updater URL points to
prices/new_data/v3/data.json. - JavaScript:
remoteDataUrlpoints toprices/new_data/v3/data.json. - Go:
RemoteDataURLpoints toprices/new_data/v3/data.json.
- Python: Default updater URL points to
Note: Ingestion APIs still support legacy arrays via shape detection if provided by a caller.