Metadata-Version: 2.4
Name: txaion-model-pricing
Version: 0.1.2
Summary: Cross-provider AI model pricing for Python
Author: Txaion LLC
License-Expression: MIT
Project-URL: Homepage, https://txaion.com
Project-URL: Repository, https://github.com/Txaion/txaion-model-pricing
Project-URL: Upstream-Data, https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
Requires-Dist: orjson<4,>=3.10
Requires-Dist: pydantic<3,>=2.13
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == "dev"
Requires-Dist: pytest<9,>=8; extra == "dev"
Requires-Dist: ruff<1,>=0.12; extra == "dev"
Dynamic: license-file

<p align="center">
  <a href="https://txaion.com">
    <img
      src="https://static.txaion.com/product/model-price/model-price-tp.png"
      alt="Txaion"
      width="360"
    >
  </a>
</p>

<h1 align="center">Txaion Model Pricing</h1>

<p align="center">
  Lightweight, cross-provider model pricing for Python.
</p>

<p align="center">
  <strong>English</strong> |
  <a href="README.zh-TW.md">繁體中文</a>
</p>

`Txaion Model Pricing` is a lightweight Python package for querying
cross-provider model information and calculating USD costs for input, output,
and cached-input tokens.

Developed and maintained by [Txaion](https://txaion.com).

The current version is `0.1.2` and requires Python 3.10 or later.

## Installation

Install the latest release from PyPI:

```bash
pip install txaion-model-pricing
```

For local development, clone the repository and install it with the development
dependencies:

```bash
python -m pip install -e ".[dev]"
```

## Quick start

```python
from txaion_model_pricing import (
    CustomPriceTable,
    add_custom_prices,
    calculate_cost,
    count_models,
    get_available_token_price_fields,
    get_model_details,
)

print(count_models())

input_cost = calculate_cost("gpt-4o", 1_000_000, "input")
output_cost = calculate_cost("gpt-4o", 1_000_000, "output")
cached_cost = calculate_cost("gpt-4o", 1_000_000, "cached")
priority_cost = calculate_cost(
    "azure_ai/gpt-5.5",
    1_000_000,
    "input_cost_per_token_priority",
)

print(input_cost)   # Decimal("2.5000000")
print(output_cost)  # Decimal("10.00000")
print(cached_cost)  # Decimal("1.25000000")
print(priority_cost)  # Decimal("10.00000")

details = get_model_details("gpt-4o")
print(details["max_input_tokens"])

fields = get_available_token_price_fields("azure_ai/gpt-5.5")
print("input_cost_per_token_priority" in fields)  # True

custom_prices = CustomPriceTable.model_validate_json(
    """
    {
      "local/my-model": {
        "input_cost_per_token": 0.000001,
        "output_cost_per_token": 0.000002,
        "max_input_tokens": 32000
      }
    }
    """
)
add_custom_prices(custom_prices)
print(calculate_cost("local/my-model", 1_000_000, "input"))  # Decimal("1.000000")
```

All costs are returned as `decimal.Decimal` values. The package does not round
results or perform currency conversion.

## Public API

### `count_models() -> int`

Returns the number of models in the merged bundled and local price tables.
Metadata entries such as `sample_spec` are excluded.

### `calculate_cost(model, tokens, token_type) -> Decimal`

Calculates the USD cost using the model's per-token price:

- `input` maps to `input_cost_per_token`
- `output` maps to `output_cost_per_token`
- `cached` maps to `cache_read_input_token_cost`

`tokens` must be a non-negative integer. If the requested price field is
unavailable, the package does not treat the cost as zero or fall back to a
different price.

In addition to the three aliases, `token_type` may be a raw scalar per-token
price field present in the model snapshot, such as
`input_cost_per_token_priority`, `output_cost_per_reasoning_token`, or
`cache_creation_input_token_cost`. Fields priced per character, image, second,
page, request, or session are not accepted, even when their names mention a
token threshold.

### `get_available_token_price_fields(model) -> tuple[str, ...]`

Returns a stable, sorted tuple of the raw scalar per-token price fields
available for the model. Use this discovery API before selecting optional or
provider-specific pricing fields.

### `get_model_details(model) -> dict`

Returns a deep copy of the model data. Modifying the returned value does not
affect the package's internal cache.

### `ModelPrice` and `CustomPriceTable`

Both are public Pydantic `BaseModel` contracts:

- `ModelPrice` validates one model's pricing fields and metadata.
- `CustomPriceTable` validates the complete JSON object keyed by model name.

```python
from pydantic import ValidationError

from txaion_model_pricing import CustomPriceTable

try:
    table = CustomPriceTable.model_validate_json(
        """
        {
          "local/my-model": {
            "input_cost_per_token": 0.000001,
            "output_cost_per_token": 0.000002
          }
        }
        """,
        strict=True,
    )
except ValidationError as exc:
    print(exc.errors())
else:
    print(table.model_dump())
    print(table.model_json_schema())
```

The models preserve LiteLLM's evolving dynamic fields while enforcing these
rules:

- The table must be a non-empty JSON object; model names cannot be blank or
  `sample_spec`.
- Each model's details must be a JSON object whose values can be represented
  faithfully in JSON.
- Fields whose names represent per-token prices must contain non-negative,
  finite numbers.
- Booleans, numeric strings, NaN, Infinity, and arbitrary Python objects are
  not valid token prices.

Direct model validation raises Pydantic `ValidationError`. Validation through
`add_custom_prices()` is normalized to the package's `InvalidPriceDataError`.

### `add_custom_prices(data, directory=None, *, overwrite=False) -> Path`

Accepts JSON text, UTF-8 bytes, a Python mapping, or a validated
`CustomPriceTable`; validates it, merges it into a local
`custom_model_prices.json`, and makes it immediately available to the current
process. The JSON root must be a non-empty object keyed by model name:

```python
from txaion_model_pricing import add_custom_prices

path = add_custom_prices(
    {
        "local/my-model": {
            "input_cost_per_token": 0.000001,
            "output_cost_per_token": 0.000002,
        }
    }
)
print(path)
```

Without `directory`, the file is stored at
`~/.txaion_model_pricing/custom_model_prices.json`. Passing a directory writes
the file there and selects that local table for the current process. Existing
models are protected by default; pass `overwrite=True` explicitly to replace
a bundled or local model with a custom entry.

Writes use a temporary file in the same directory followed by an atomic
replacement, so validation failures do not modify the existing file. Token
prices must be non-negative, finite JSON numbers. Booleans, strings, NaN, and
Infinity are rejected.

### `set_local_price_directory(directory) -> Path`

Selects the local price-table directory for the current process and returns
the full file path. If a non-default directory is used, select it again before
querying in a later Python process:

```python
from txaion_model_pricing import set_local_price_directory

set_local_price_directory("./pricing-data")
```

The directory may be selected before `custom_model_prices.json` exists; a
later `add_custom_prices(...)` call will create it.

## Error handling

All domain-specific exceptions inherit from `ModelPriceError`:

```python
from txaion_model_pricing import (
    InvalidPriceDataError,
    InvalidTokenCountError,
    InvalidTokenTypeError,
    ModelAlreadyExistsError,
    NotFound,
    PriceUnavailableError,
    calculate_cost,
)

try:
    cost = calculate_cost("unknown-model", 1_000, "input")
except NotFound:
    ...
except PriceUnavailableError:
    # The field name is valid, but this model does not provide that price.
    ...
except (InvalidTokenCountError, InvalidTokenTypeError):
    # The token count or price-field name is invalid.
    ...
except (InvalidPriceDataError, ModelAlreadyExistsError):
    # The custom JSON is invalid or would overwrite an existing model.
    ...
```

## Price data

The package includes a pinned snapshot of the
[LiteLLM model price and context-window data](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json).
The source commit, retrieval time, and SHA-256 checksum are recorded in
`src/txaion_model_pricing/data_source.json`. See `THIRD_PARTY_NOTICES.md` for
third-party attribution and licensing information.

The local `custom_model_prices.json` is an independent user-data layer.
Local entries take precedence for matching model names, and package upgrades
do not rewrite this file.

Maintainers must specify an immutable commit SHA or tag when updating the
snapshot:

```bash
python scripts/update_prices.py <commit-or-tag>
```

The update tool downloads and validates the JSON before atomically replacing
the snapshot and its metadata. Moving refs such as `main`, `master`, and
`latest` are rejected.

## Limitations

- Version `0.1.2` only calculates costs from scalar per-token fields.
- Image-, character-, second-, page-, request-, session-, search-, tool-call-,
  and storage-based pricing are not supported by `calculate_cost()`. Token
  fields for audio content are supported when the snapshot expresses their
  unit as a per-token price.
- Model providers may change their prices at any time. Results depend on the
  bundled snapshot and may not match current provider pricing. Verify official
  provider prices before using the results for billing or budget enforcement.

## Development

```bash
ruff check .
pytest
python -m build
```

## License

This project and the vendored LiteLLM data are redistributed under the MIT
License. See `LICENSE` and `THIRD_PARTY_NOTICES.md` for details.
