GenAI Prices

repository·main·Indexed 43 days ago

https://github.com/pydantic/genai-prices

Tools 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.

Tokens
43.2K
Snippets
127
Records
179
Agent score
57%

What's inside GenAI Prices

  1. Overview of GenAI Prices

    main
    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).
  2. How usage value reads and errors work

    main

    When 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 0 without 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.

  3. Understand v3 provider contract evolution

    main

    The 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.
  4. Understand the Phase 1 Static Unit Registry architecture

    main

    Phase 1 of the genai-prices registry introduces a static, immutable UnitRegistry that 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 UnitRegistry is constructed once from bundled data (data_units.py or dataUnits.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.
  5. Handle fractional usage values in `Usage`

    main

    The Usage object supports int, float, and Decimal for all reportable units (e.g., input_tokens, audio_seconds).

    • Precision: If any operand in an arithmetic operation is a Decimal, the result will be a Decimal. Otherwise, it follows standard Python float/int rules.
    • Audio Usage: For Groq's Whisper models, report duration using audio_seconds or input_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 standard json encoder 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')
  6. How unit dimensions and hierarchies work

    main

    Dimensions 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_write unit is a descendant of a more general tokens ancestor.
    • 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.

  7. Handle overlapping modalities with Join Coverage (Mobius inversion)

    main

    Some 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:

    1. cache_audio_read_tokens: 50
    2. cache_read_tokens: 200 - 50 = 150
    3. input_audio_tokens: 300 - 50 = 250
    4. input_tokens: 1000 - 200 - 300 + 50 = 550

    Critical Note: If cache_audio_read_tokens is omitted while both cache_read_tokens and input_audio_tokens are positive, the runtime will raise an error rather than assuming the overlap is zero.

  8. Understand the unit decomposition and catch-all pricing model

    main

    The 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_tokens is 1000 and input_audio_tokens is 300, the system calculates 700 tokens at the base input_mtok rate.
    • 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.
  9. How Go decodes and manages pricing data with the immutable calculator

    main

    The 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 Calculator and are appended to the Warnings field of PriceCalculation or ExtractedUsage results 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 = newCalc
  10. Use fractional usage values for non-token units

    main

    The 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_seconds or input_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)
  11. Understand the v3 Data Contract and Runtime Defaults

    main

    The 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: remoteDataUrl points to prices/new_data/v3/data.json.
      • Go: RemoteDataURL points to prices/new_data/v3/data.json.

    Note: Ingestion APIs still support legacy arrays via shape detection if provided by a caller.