metergraph-core 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- metergraph_core-0.1.0/MANIFEST.in +4 -0
- metergraph_core-0.1.0/PKG-INFO +110 -0
- metergraph_core-0.1.0/README.md +98 -0
- metergraph_core-0.1.0/pyproject.toml +23 -0
- metergraph_core-0.1.0/setup.cfg +4 -0
- metergraph_core-0.1.0/src/metergraph_core/__init__.py +21 -0
- metergraph_core-0.1.0/src/metergraph_core/catalog.py +275 -0
- metergraph_core-0.1.0/src/metergraph_core/data/prices.yaml +886 -0
- metergraph_core-0.1.0/src/metergraph_core/loader.py +167 -0
- metergraph_core-0.1.0/src/metergraph_core.egg-info/PKG-INFO +110 -0
- metergraph_core-0.1.0/src/metergraph_core.egg-info/SOURCES.txt +12 -0
- metergraph_core-0.1.0/src/metergraph_core.egg-info/dependency_links.txt +1 -0
- metergraph_core-0.1.0/src/metergraph_core.egg-info/requires.txt +5 -0
- metergraph_core-0.1.0/src/metergraph_core.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metergraph-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable MeterGraph catalog and deterministic token-cost pricing engine
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# metergraph-core
|
|
14
|
+
|
|
15
|
+
Reusable catalog and deterministic token-cost pricing engine for MeterGraph.
|
|
16
|
+
|
|
17
|
+
`metergraph-core` owns the public effective-dated model catalog and the pricing
|
|
18
|
+
logic shared across MeterGraph systems: catalog parsing and validation, provider
|
|
19
|
+
and model alias resolution, channel and region selection, input/output/cache/
|
|
20
|
+
batch/long-context pricing rules, deterministic cost calculation with reason
|
|
21
|
+
codes, stable logical price identifiers, and catalog version and content-hash
|
|
22
|
+
reporting.
|
|
23
|
+
|
|
24
|
+
It does not own HTTP routes, database access, migrations, authentication,
|
|
25
|
+
tenancy, ingest, dashboard code, or any hosted-only concern, and it never reads
|
|
26
|
+
server environment variables.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install metergraph-core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from datetime import datetime, timezone
|
|
38
|
+
|
|
39
|
+
from metergraph_core import load_catalog
|
|
40
|
+
|
|
41
|
+
catalog = load_catalog(region="global")
|
|
42
|
+
result = catalog.snapshot.cost(
|
|
43
|
+
provider="openai",
|
|
44
|
+
model="gpt-5.4-mini",
|
|
45
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
46
|
+
input_tokens=1000,
|
|
47
|
+
output_tokens=200,
|
|
48
|
+
)
|
|
49
|
+
print(result.canonical_model, result.price_id, result.cost_usd, result.status)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`load_catalog()` loads the catalog bundled in the installed package. Pass an
|
|
53
|
+
explicit `path` for tests or a self-hosted catalog replacement. The returned
|
|
54
|
+
`LoadedCatalog` exposes the declared catalog `version`, the SHA-256
|
|
55
|
+
`content_hash` of the loaded bytes, the parsed `document`, and the immutable
|
|
56
|
+
`snapshot` used for pricing.
|
|
57
|
+
|
|
58
|
+
Planning and evaluation systems that know the deployment channel before making
|
|
59
|
+
a call can resolve the exact effective price without emulating a provider
|
|
60
|
+
response:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
price = catalog.snapshot.resolve_price(
|
|
64
|
+
model="openai/gpt-5.6-luna",
|
|
65
|
+
channel="vercel-ai-gateway",
|
|
66
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
67
|
+
)
|
|
68
|
+
if price is not None:
|
|
69
|
+
print(
|
|
70
|
+
price.canonical_model,
|
|
71
|
+
price.price.id,
|
|
72
|
+
price.price.input_per_mtok,
|
|
73
|
+
price.price.source_url,
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Resolution accepts canonical IDs and channel-scoped aliases, normalizes case
|
|
78
|
+
and surrounding whitespace, applies the configured region fallback and
|
|
79
|
+
effective-date windows, and returns `None` when no exact model/channel price
|
|
80
|
+
exists. It never substitutes a direct-provider price for a gateway price.
|
|
81
|
+
|
|
82
|
+
`LoadedCatalog.currency` is currently always `USD`, and
|
|
83
|
+
`LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
|
|
84
|
+
checked against its linked provider sources.
|
|
85
|
+
|
|
86
|
+
## Public API
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from metergraph_core import (
|
|
90
|
+
Alias,
|
|
91
|
+
CatalogError,
|
|
92
|
+
CatalogSnapshot,
|
|
93
|
+
CostResult,
|
|
94
|
+
LoadedCatalog,
|
|
95
|
+
Price,
|
|
96
|
+
ResolvedPrice,
|
|
97
|
+
load_catalog,
|
|
98
|
+
parse_catalog,
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Catalog maintenance
|
|
103
|
+
|
|
104
|
+
The only manually maintained public catalog lives at
|
|
105
|
+
`src/metergraph_core/data/prices.yaml`. Every record requires its provider
|
|
106
|
+
source URL and effective date. Corrections close or add effective windows; they
|
|
107
|
+
never rewrite historical prices in place. A catalog change updates the declared
|
|
108
|
+
catalog version and produces a patch release of `metergraph-core`. Software
|
|
109
|
+
version and catalog version are separate values because code and price data have
|
|
110
|
+
different lifecycles.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# metergraph-core
|
|
2
|
+
|
|
3
|
+
Reusable catalog and deterministic token-cost pricing engine for MeterGraph.
|
|
4
|
+
|
|
5
|
+
`metergraph-core` owns the public effective-dated model catalog and the pricing
|
|
6
|
+
logic shared across MeterGraph systems: catalog parsing and validation, provider
|
|
7
|
+
and model alias resolution, channel and region selection, input/output/cache/
|
|
8
|
+
batch/long-context pricing rules, deterministic cost calculation with reason
|
|
9
|
+
codes, stable logical price identifiers, and catalog version and content-hash
|
|
10
|
+
reporting.
|
|
11
|
+
|
|
12
|
+
It does not own HTTP routes, database access, migrations, authentication,
|
|
13
|
+
tenancy, ingest, dashboard code, or any hosted-only concern, and it never reads
|
|
14
|
+
server environment variables.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python -m pip install metergraph-core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
|
|
27
|
+
from metergraph_core import load_catalog
|
|
28
|
+
|
|
29
|
+
catalog = load_catalog(region="global")
|
|
30
|
+
result = catalog.snapshot.cost(
|
|
31
|
+
provider="openai",
|
|
32
|
+
model="gpt-5.4-mini",
|
|
33
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
34
|
+
input_tokens=1000,
|
|
35
|
+
output_tokens=200,
|
|
36
|
+
)
|
|
37
|
+
print(result.canonical_model, result.price_id, result.cost_usd, result.status)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`load_catalog()` loads the catalog bundled in the installed package. Pass an
|
|
41
|
+
explicit `path` for tests or a self-hosted catalog replacement. The returned
|
|
42
|
+
`LoadedCatalog` exposes the declared catalog `version`, the SHA-256
|
|
43
|
+
`content_hash` of the loaded bytes, the parsed `document`, and the immutable
|
|
44
|
+
`snapshot` used for pricing.
|
|
45
|
+
|
|
46
|
+
Planning and evaluation systems that know the deployment channel before making
|
|
47
|
+
a call can resolve the exact effective price without emulating a provider
|
|
48
|
+
response:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
price = catalog.snapshot.resolve_price(
|
|
52
|
+
model="openai/gpt-5.6-luna",
|
|
53
|
+
channel="vercel-ai-gateway",
|
|
54
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
55
|
+
)
|
|
56
|
+
if price is not None:
|
|
57
|
+
print(
|
|
58
|
+
price.canonical_model,
|
|
59
|
+
price.price.id,
|
|
60
|
+
price.price.input_per_mtok,
|
|
61
|
+
price.price.source_url,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Resolution accepts canonical IDs and channel-scoped aliases, normalizes case
|
|
66
|
+
and surrounding whitespace, applies the configured region fallback and
|
|
67
|
+
effective-date windows, and returns `None` when no exact model/channel price
|
|
68
|
+
exists. It never substitutes a direct-provider price for a gateway price.
|
|
69
|
+
|
|
70
|
+
`LoadedCatalog.currency` is currently always `USD`, and
|
|
71
|
+
`LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
|
|
72
|
+
checked against its linked provider sources.
|
|
73
|
+
|
|
74
|
+
## Public API
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from metergraph_core import (
|
|
78
|
+
Alias,
|
|
79
|
+
CatalogError,
|
|
80
|
+
CatalogSnapshot,
|
|
81
|
+
CostResult,
|
|
82
|
+
LoadedCatalog,
|
|
83
|
+
Price,
|
|
84
|
+
ResolvedPrice,
|
|
85
|
+
load_catalog,
|
|
86
|
+
parse_catalog,
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Catalog maintenance
|
|
91
|
+
|
|
92
|
+
The only manually maintained public catalog lives at
|
|
93
|
+
`src/metergraph_core/data/prices.yaml`. Every record requires its provider
|
|
94
|
+
source URL and effective date. Corrections close or add effective windows; they
|
|
95
|
+
never rewrite historical prices in place. A catalog change updates the declared
|
|
96
|
+
catalog version and produces a patch release of `metergraph-core`. Software
|
|
97
|
+
version and catalog version are separate values because code and price data have
|
|
98
|
+
different lifecycles.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "metergraph-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Reusable MeterGraph catalog and deterministic token-cost pricing engine"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pyyaml>=6.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = ["build>=1.2", "pytest>=8"]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["setuptools>=68"]
|
|
17
|
+
build-backend = "setuptools.build_meta"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
metergraph_core = ["data/prices.yaml"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Reusable MeterGraph catalog and deterministic token-cost pricing core."""
|
|
2
|
+
|
|
3
|
+
from .catalog import Alias, CatalogSnapshot, CostResult, Price, ResolvedPrice
|
|
4
|
+
from .loader import (
|
|
5
|
+
CatalogError,
|
|
6
|
+
LoadedCatalog,
|
|
7
|
+
load_catalog,
|
|
8
|
+
parse_catalog,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Alias",
|
|
13
|
+
"CatalogError",
|
|
14
|
+
"CatalogSnapshot",
|
|
15
|
+
"CostResult",
|
|
16
|
+
"LoadedCatalog",
|
|
17
|
+
"Price",
|
|
18
|
+
"ResolvedPrice",
|
|
19
|
+
"load_catalog",
|
|
20
|
+
"parse_catalog",
|
|
21
|
+
]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Effective-dated model catalog and deterministic token-cost enrichment."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
_MILLION = Decimal("1000000")
|
|
10
|
+
_COST_QUANTUM = Decimal("0.00000001")
|
|
11
|
+
_PROVIDER_ALIASES = {
|
|
12
|
+
"amazon-bedrock": "bedrock",
|
|
13
|
+
"aws": "bedrock",
|
|
14
|
+
"aws-bedrock": "bedrock",
|
|
15
|
+
"gemini": "google",
|
|
16
|
+
"google-genai": "google",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Alias:
|
|
22
|
+
model_id: str
|
|
23
|
+
canonical_id: str
|
|
24
|
+
pricing_channel: str
|
|
25
|
+
rules: Mapping[str, Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Price:
|
|
30
|
+
id: str
|
|
31
|
+
model_id: str
|
|
32
|
+
pricing_channel: str
|
|
33
|
+
region: str
|
|
34
|
+
input_per_mtok: Decimal | None
|
|
35
|
+
output_per_mtok: Decimal | None
|
|
36
|
+
cache_read_per_mtok: Decimal | None
|
|
37
|
+
cache_write_5m_per_mtok: Decimal | None
|
|
38
|
+
cache_write_1h_per_mtok: Decimal | None
|
|
39
|
+
batch_input_per_mtok: Decimal | None
|
|
40
|
+
batch_output_per_mtok: Decimal | None
|
|
41
|
+
rules: Mapping[str, Any]
|
|
42
|
+
effective_from: datetime
|
|
43
|
+
effective_to: datetime | None
|
|
44
|
+
source_url: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class CostResult:
|
|
49
|
+
canonical_model: str | None
|
|
50
|
+
price_id: str | None
|
|
51
|
+
cost_usd: Decimal | None
|
|
52
|
+
status: str
|
|
53
|
+
reasons: tuple[str, ...] = ()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class ResolvedPrice:
|
|
58
|
+
"""An effective price selected for a planned model deployment."""
|
|
59
|
+
|
|
60
|
+
canonical_model: str
|
|
61
|
+
price: Price
|
|
62
|
+
rules: Mapping[str, Any]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _decimal(value: Any) -> Decimal | None:
|
|
66
|
+
if value is None:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
result = Decimal(str(value))
|
|
70
|
+
except (InvalidOperation, ValueError, TypeError):
|
|
71
|
+
return None
|
|
72
|
+
return result if result.is_finite() else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _tokens(value: Any) -> int | None:
|
|
76
|
+
if value is None or isinstance(value, bool):
|
|
77
|
+
return None
|
|
78
|
+
try:
|
|
79
|
+
result = int(value)
|
|
80
|
+
except (ValueError, TypeError, OverflowError):
|
|
81
|
+
return None
|
|
82
|
+
return result if result >= 0 else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CatalogSnapshot:
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
aliases: Mapping[tuple[str, str], Alias],
|
|
89
|
+
prices: list[Price],
|
|
90
|
+
*,
|
|
91
|
+
region: str,
|
|
92
|
+
) -> None:
|
|
93
|
+
self._aliases = dict(aliases)
|
|
94
|
+
self._prices: dict[tuple[str, str], list[Price]] = {}
|
|
95
|
+
self._deployment_aliases: dict[tuple[str, str], Alias] = {}
|
|
96
|
+
self._region = region.strip().lower()
|
|
97
|
+
for (_, observed_model), alias in aliases.items():
|
|
98
|
+
channel = alias.pricing_channel.strip().lower()
|
|
99
|
+
for model in (observed_model, alias.canonical_id):
|
|
100
|
+
key = (model.strip().lower(), channel)
|
|
101
|
+
existing = self._deployment_aliases.get(key)
|
|
102
|
+
if existing is not None and (
|
|
103
|
+
existing.canonical_id != alias.canonical_id
|
|
104
|
+
or dict(existing.rules) != dict(alias.rules)
|
|
105
|
+
):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"ambiguous deployment alias {model!r} for channel {channel!r}"
|
|
108
|
+
)
|
|
109
|
+
self._deployment_aliases[key] = alias
|
|
110
|
+
for price in prices:
|
|
111
|
+
self._prices.setdefault((price.model_id, price.pricing_channel), []).append(
|
|
112
|
+
price
|
|
113
|
+
)
|
|
114
|
+
for candidates in self._prices.values():
|
|
115
|
+
candidates.sort(key=lambda price: price.effective_from, reverse=True)
|
|
116
|
+
|
|
117
|
+
def _price_for(self, alias: Alias, at: datetime) -> Price | None:
|
|
118
|
+
at = at if at.tzinfo else at.replace(tzinfo=timezone.utc)
|
|
119
|
+
candidates = self._prices.get((alias.model_id, alias.pricing_channel), [])
|
|
120
|
+
for region in dict.fromkeys((self._region, "*", "global")):
|
|
121
|
+
for price in candidates:
|
|
122
|
+
if price.region.lower() != region:
|
|
123
|
+
continue
|
|
124
|
+
if price.effective_from <= at and (
|
|
125
|
+
price.effective_to is None or at < price.effective_to
|
|
126
|
+
):
|
|
127
|
+
return price
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def resolve_price(
|
|
131
|
+
self, *, model: Any, channel: Any, at: datetime
|
|
132
|
+
) -> ResolvedPrice | None:
|
|
133
|
+
"""Resolve pricing for an explicitly selected model and channel.
|
|
134
|
+
|
|
135
|
+
This is intended for planners and evaluation pipelines that know the
|
|
136
|
+
deployment channel before making a provider call. It never falls back
|
|
137
|
+
to pricing from a different channel.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
model_key = str(model or "").strip().lower()
|
|
141
|
+
channel_key = str(channel or "").strip().lower()
|
|
142
|
+
alias = self._deployment_aliases.get((model_key, channel_key))
|
|
143
|
+
if alias is None:
|
|
144
|
+
return None
|
|
145
|
+
price = self._price_for(alias, at)
|
|
146
|
+
if price is None:
|
|
147
|
+
return None
|
|
148
|
+
return ResolvedPrice(
|
|
149
|
+
canonical_model=alias.canonical_id,
|
|
150
|
+
price=price,
|
|
151
|
+
rules=MappingProxyType({**price.rules, **alias.rules}),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def cost(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
provider: Any,
|
|
158
|
+
model: Any,
|
|
159
|
+
at: datetime,
|
|
160
|
+
input_tokens: Any,
|
|
161
|
+
output_tokens: Any,
|
|
162
|
+
cache_read_tokens: Any = None,
|
|
163
|
+
cache_write_tokens: Any = None,
|
|
164
|
+
batch: bool = False,
|
|
165
|
+
) -> CostResult:
|
|
166
|
+
provider_key = str(provider or "").strip().lower()
|
|
167
|
+
provider_key = _PROVIDER_ALIASES.get(provider_key, provider_key)
|
|
168
|
+
model_key = str(model or "").strip().lower()
|
|
169
|
+
alias = self._aliases.get((provider_key, model_key))
|
|
170
|
+
if alias is None:
|
|
171
|
+
return CostResult(None, None, None, "unpriced", ("unknown_model",))
|
|
172
|
+
price = self._price_for(alias, at)
|
|
173
|
+
if price is None:
|
|
174
|
+
return CostResult(
|
|
175
|
+
alias.canonical_id,
|
|
176
|
+
None,
|
|
177
|
+
None,
|
|
178
|
+
"unpriced",
|
|
179
|
+
("no_effective_price",),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
reasons: list[str] = []
|
|
183
|
+
input_count = _tokens(input_tokens)
|
|
184
|
+
output_count = _tokens(output_tokens)
|
|
185
|
+
cache_read_count = _tokens(cache_read_tokens) or 0
|
|
186
|
+
cache_write_count = _tokens(cache_write_tokens) or 0
|
|
187
|
+
if input_count is None:
|
|
188
|
+
reasons.append("missing_input_tokens")
|
|
189
|
+
input_count = 0
|
|
190
|
+
if output_count is None:
|
|
191
|
+
reasons.append("missing_output_tokens")
|
|
192
|
+
output_count = 0
|
|
193
|
+
|
|
194
|
+
rules = {**price.rules, **alias.rules}
|
|
195
|
+
billable_input = input_count
|
|
196
|
+
deducted_input = 0
|
|
197
|
+
if rules.get("input_includes_cache_read"):
|
|
198
|
+
if cache_read_count > input_count:
|
|
199
|
+
reasons.append("cache_read_exceeds_input")
|
|
200
|
+
deducted_input = input_count
|
|
201
|
+
else:
|
|
202
|
+
deducted_input += cache_read_count
|
|
203
|
+
if rules.get("input_includes_cache_write"):
|
|
204
|
+
if cache_write_count > input_count - deducted_input:
|
|
205
|
+
reasons.append("cache_write_exceeds_input")
|
|
206
|
+
deducted_input = input_count
|
|
207
|
+
else:
|
|
208
|
+
deducted_input += cache_write_count
|
|
209
|
+
billable_input -= deducted_input
|
|
210
|
+
|
|
211
|
+
input_rate = price.input_per_mtok
|
|
212
|
+
output_rate = price.output_per_mtok
|
|
213
|
+
if batch:
|
|
214
|
+
if (
|
|
215
|
+
price.batch_input_per_mtok is None
|
|
216
|
+
or price.batch_output_per_mtok is None
|
|
217
|
+
):
|
|
218
|
+
reasons.append("batch_rate_unavailable")
|
|
219
|
+
else:
|
|
220
|
+
input_rate = price.batch_input_per_mtok
|
|
221
|
+
output_rate = price.batch_output_per_mtok
|
|
222
|
+
|
|
223
|
+
input_multiplier = Decimal("1")
|
|
224
|
+
output_multiplier = Decimal("1")
|
|
225
|
+
long_context = rules.get("long_context") or {}
|
|
226
|
+
threshold = _tokens(long_context.get("threshold"))
|
|
227
|
+
if threshold is not None and input_count > threshold:
|
|
228
|
+
input_multiplier = _decimal(
|
|
229
|
+
long_context.get("input_multiplier")
|
|
230
|
+
) or Decimal("1")
|
|
231
|
+
output_multiplier = _decimal(
|
|
232
|
+
long_context.get("output_multiplier")
|
|
233
|
+
) or Decimal("1")
|
|
234
|
+
|
|
235
|
+
cost = Decimal("0")
|
|
236
|
+
if input_rate is None:
|
|
237
|
+
if billable_input:
|
|
238
|
+
reasons.append("input_rate_unavailable")
|
|
239
|
+
else:
|
|
240
|
+
cost += Decimal(billable_input) * input_rate * input_multiplier / _MILLION
|
|
241
|
+
if output_rate is None:
|
|
242
|
+
if output_count:
|
|
243
|
+
reasons.append("output_rate_unavailable")
|
|
244
|
+
else:
|
|
245
|
+
cost += Decimal(output_count) * output_rate * output_multiplier / _MILLION
|
|
246
|
+
if cache_read_count:
|
|
247
|
+
if price.cache_read_per_mtok is None:
|
|
248
|
+
reasons.append("cache_read_rate_unavailable")
|
|
249
|
+
else:
|
|
250
|
+
cost += (
|
|
251
|
+
Decimal(cache_read_count)
|
|
252
|
+
* price.cache_read_per_mtok
|
|
253
|
+
* input_multiplier
|
|
254
|
+
/ _MILLION
|
|
255
|
+
)
|
|
256
|
+
if cache_write_count:
|
|
257
|
+
if price.cache_write_5m_per_mtok is None:
|
|
258
|
+
reasons.append("cache_write_rate_unavailable")
|
|
259
|
+
else:
|
|
260
|
+
cost += (
|
|
261
|
+
Decimal(cache_write_count)
|
|
262
|
+
* price.cache_write_5m_per_mtok
|
|
263
|
+
* input_multiplier
|
|
264
|
+
/ _MILLION
|
|
265
|
+
)
|
|
266
|
+
if rules.get("uncaptured_fees"):
|
|
267
|
+
reasons.append("uncaptured_fees")
|
|
268
|
+
|
|
269
|
+
return CostResult(
|
|
270
|
+
alias.canonical_id,
|
|
271
|
+
price.id,
|
|
272
|
+
cost.quantize(_COST_QUANTUM, rounding=ROUND_HALF_UP),
|
|
273
|
+
"partial" if reasons else "priced",
|
|
274
|
+
tuple(dict.fromkeys(reasons)),
|
|
275
|
+
)
|