atif-sql 0.2.0__py3-none-any.whl
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.
- atif_analytics/__init__.py +9 -0
- atif_analytics/application/__init__.py +3 -0
- atif_analytics/application/analyze.py +158 -0
- atif_analytics/application/prompts.py +750 -0
- atif_analytics/application/use_cases/__init__.py +3 -0
- atif_analytics/application/use_cases/_shared.py +264 -0
- atif_analytics/application/use_cases/classify.py +430 -0
- atif_analytics/application/use_cases/conflicts.py +448 -0
- atif_analytics/application/use_cases/friction.py +634 -0
- atif_analytics/application/use_cases/perceived.py +451 -0
- atif_analytics/domain/__init__.py +3 -0
- atif_analytics/domain/authorship.py +166 -0
- atif_analytics/domain/config.py +30 -0
- atif_analytics/domain/costs.py +50 -0
- atif_analytics/domain/friction.py +117 -0
- atif_analytics/domain/layout.py +94 -0
- atif_analytics/domain/models.py +385 -0
- atif_analytics/domain/ports.py +100 -0
- atif_analytics/domain/transcript.py +359 -0
- atif_analytics/infrastructure/__init__.py +3 -0
- atif_analytics/infrastructure/corpus_reader.py +546 -0
- atif_analytics/infrastructure/parquet_cache.py +186 -0
- atif_analytics/infrastructure/settings.py +114 -0
- atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
- atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
- atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
- atif_analytics/py.typed +0 -0
- atif_cli/__init__.py +3 -0
- atif_cli/__main__.py +8 -0
- atif_cli/app.py +2459 -0
- atif_cli/converter_adapter.py +151 -0
- atif_cli/corpus.py +409 -0
- atif_cli/cron.py +311 -0
- atif_cli/duck_errors.py +97 -0
- atif_cli/embed_lake.py +101 -0
- atif_cli/errors.py +89 -0
- atif_cli/lake.py +373 -0
- atif_cli/lake_sessions.py +270 -0
- atif_cli/output.py +297 -0
- atif_cli/py.typed +0 -0
- atif_converter/__init__.py +3 -0
- atif_converter/application/__init__.py +3 -0
- atif_converter/application/convert_and_audit.py +294 -0
- atif_converter/application/convert_codex.py +210 -0
- atif_converter/domain/__init__.py +9 -0
- atif_converter/domain/agents.py +40 -0
- atif_converter/domain/atif/__init__.py +55 -0
- atif_converter/domain/atif/agent.py +37 -0
- atif_converter/domain/atif/content.py +161 -0
- atif_converter/domain/atif/final_metrics.py +45 -0
- atif_converter/domain/atif/metrics.py +49 -0
- atif_converter/domain/atif/observation.py +21 -0
- atif_converter/domain/atif/observation_result.py +47 -0
- atif_converter/domain/atif/step.py +144 -0
- atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
- atif_converter/domain/atif/tool_call.py +36 -0
- atif_converter/domain/atif/trajectory.py +211 -0
- atif_converter/domain/atif/trajectory_validator.py +299 -0
- atif_converter/domain/blobs.py +510 -0
- atif_converter/domain/claude_code_conversion.py +1077 -0
- atif_converter/domain/codex_conversion.py +1036 -0
- atif_converter/domain/codex_edges.py +192 -0
- atif_converter/domain/codex_enrichment.py +419 -0
- atif_converter/domain/codex_fidelity.py +124 -0
- atif_converter/domain/codex_nested_calls.py +165 -0
- atif_converter/domain/edges.py +113 -0
- atif_converter/domain/enrichment.py +412 -0
- atif_converter/domain/errors.py +60 -0
- atif_converter/domain/fidelity.py +171 -0
- atif_converter/domain/model_prices.json +8538 -0
- atif_converter/domain/pricing.py +790 -0
- atif_converter/domain/result_signals.py +774 -0
- atif_converter/domain/schema_version.py +90 -0
- atif_converter/domain/session_events.py +408 -0
- atif_converter/infrastructure/__init__.py +3 -0
- atif_converter/infrastructure/census.py +89 -0
- atif_converter/infrastructure/claude_code_converter.py +127 -0
- atif_converter/infrastructure/codex_adapter.py +132 -0
- atif_converter/infrastructure/codex_census.py +100 -0
- atif_converter/infrastructure/codex_converter.py +82 -0
- atif_converter/infrastructure/harbor_adapter.py +187 -0
- atif_converter/infrastructure/raw_records.py +426 -0
- atif_converter/infrastructure/source_archive.py +154 -0
- atif_converter/py.typed +0 -0
- atif_corpus/__init__.py +13 -0
- atif_corpus/application/__init__.py +7 -0
- atif_corpus/application/materialize.py +1826 -0
- atif_corpus/domain/__init__.py +9 -0
- atif_corpus/domain/agents.py +33 -0
- atif_corpus/domain/generation.py +34 -0
- atif_corpus/domain/layout.py +168 -0
- atif_corpus/domain/ports.py +175 -0
- atif_corpus/domain/session_id.py +68 -0
- atif_corpus/domain/sessions.py +228 -0
- atif_corpus/domain/slug.py +72 -0
- atif_corpus/domain/source_layout.py +143 -0
- atif_corpus/domain/watermark.py +76 -0
- atif_corpus/infrastructure/__init__.py +7 -0
- atif_corpus/infrastructure/atomic.py +266 -0
- atif_corpus/infrastructure/compress_artifacts.py +161 -0
- atif_corpus/infrastructure/fake_converter.py +174 -0
- atif_corpus/infrastructure/scanner.py +316 -0
- atif_corpus/infrastructure/settings.py +130 -0
- atif_corpus/infrastructure/source_archive.py +202 -0
- atif_corpus/py.typed +0 -0
- atif_duck/__init__.py +43 -0
- atif_duck/domain/__init__.py +32 -0
- atif_duck/domain/artifacts.py +66 -0
- atif_duck/domain/authorship.py +165 -0
- atif_duck/domain/catalog.py +626 -0
- atif_duck/domain/columnar.py +149 -0
- atif_duck/domain/embedding_guard.py +88 -0
- atif_duck/domain/examples.py +265 -0
- atif_duck/domain/lake.py +427 -0
- atif_duck/domain/lake_sessions.py +97 -0
- atif_duck/domain/raw_readers.py +140 -0
- atif_duck/domain/session_id.py +69 -0
- atif_duck/domain/sql_literal.py +50 -0
- atif_duck/infrastructure/__init__.py +34 -0
- atif_duck/infrastructure/analytics.py +469 -0
- atif_duck/infrastructure/authorship.py +139 -0
- atif_duck/infrastructure/columnar.py +641 -0
- atif_duck/infrastructure/lake.py +1383 -0
- atif_duck/infrastructure/lake_sessions.py +194 -0
- atif_duck/infrastructure/lake_settings.py +63 -0
- atif_duck/infrastructure/lake_steps.py +225 -0
- atif_duck/infrastructure/projections.py +221 -0
- atif_duck/infrastructure/registry.py +2194 -0
- atif_duck/infrastructure/stored_artifacts.py +115 -0
- atif_duck/py.typed +0 -0
- atif_embed/__init__.py +12 -0
- atif_embed/application/__init__.py +3 -0
- atif_embed/application/embed.py +293 -0
- atif_embed/application/prune.py +80 -0
- atif_embed/domain/__init__.py +3 -0
- atif_embed/domain/discovery.py +70 -0
- atif_embed/domain/embedding_guard.py +83 -0
- atif_embed/domain/errors.py +94 -0
- atif_embed/domain/ports.py +223 -0
- atif_embed/domain/sql_literal.py +49 -0
- atif_embed/domain/text_stamp.py +71 -0
- atif_embed/infrastructure/__init__.py +3 -0
- atif_embed/infrastructure/cohere_bedrock.py +433 -0
- atif_embed/infrastructure/corpus_text_rows.py +273 -0
- atif_embed/infrastructure/gemma_local.py +355 -0
- atif_embed/infrastructure/lake_text_rows.py +203 -0
- atif_embed/infrastructure/lance_store.py +440 -0
- atif_embed/infrastructure/providers.py +30 -0
- atif_embed/infrastructure/settings.py +154 -0
- atif_embed/py.typed +0 -0
- atif_models/__init__.py +14 -0
- atif_models/domain/__init__.py +9 -0
- atif_models/domain/ports.py +143 -0
- atif_models/domain/registry.py +151 -0
- atif_models/domain/schema.py +109 -0
- atif_models/infrastructure/__init__.py +8 -0
- atif_models/infrastructure/openai_bedrock.py +314 -0
- atif_models/infrastructure/settings.py +85 -0
- atif_models/py.typed +0 -0
- atif_sql-0.2.0.dist-info/METADATA +272 -0
- atif_sql-0.2.0.dist-info/RECORD +164 -0
- atif_sql-0.2.0.dist-info/WHEEL +4 -0
- atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
- atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Per-call USD pricing: litellm's ``cost_per_token`` arithmetic over a vendored price table.
|
|
4
|
+
|
|
5
|
+
WHY. harbor priced every step through ``litellm.cost_per_token``. litellm is a
|
|
6
|
+
large install and ``import litellm`` costs about four seconds, all for one
|
|
7
|
+
multiply-and-add per step, so atif-sql doesn't depend on it at all. This module
|
|
8
|
+
reads :data:`TABLE_FILENAME`, a filtered copy of litellm's public
|
|
9
|
+
``model_prices_and_context_window.json`` (MIT, notice and source ref in the
|
|
10
|
+
file's ``meta`` block) regenerated by hand with ``scripts/update_prices.py``,
|
|
11
|
+
and repeats litellm 1.103.2's arithmetic in the same float operation order.
|
|
12
|
+
For every entry the table carries, the output is bit for bit what litellm
|
|
13
|
+
returns from the same data, which ``tests/test_pricing_identity.py`` proves
|
|
14
|
+
against litellm itself wherever litellm is installed as a dev dependency.
|
|
15
|
+
|
|
16
|
+
WHAT IS COVERED. The table holds the Claude and OpenAI text models (bare ids,
|
|
17
|
+
Bedrock-style ids, and ``anthropic/`` / ``openai/`` / ``bedrock/`` prefixed
|
|
18
|
+
keys) whose ``litellm_provider`` is ``anthropic``, ``openai``, ``bedrock`` or
|
|
19
|
+
``bedrock_converse``. A model name with no ``/`` is priced when it is either
|
|
20
|
+
|
|
21
|
+
- an exact key of the table, or
|
|
22
|
+
- absent from it but routed to Anthropic by the table's own
|
|
23
|
+
``fallback_generalizations`` rules (a bare ``claude-<family>-<n>`` id nobody
|
|
24
|
+
has priced yet). litellm prices those from the capability rules' union,
|
|
25
|
+
which carries no rates, so the result is ``(0.0, 0.0)``.
|
|
26
|
+
:func:`fast_cost_per_token` keeps returning exactly that, because it's
|
|
27
|
+
litellm's answer and the identity test holds it to litellm. The converter
|
|
28
|
+
prices through :func:`priced_cost_per_token` instead, which reads that
|
|
29
|
+
rate-less entry as "no price" and returns ``None``: a model nobody priced
|
|
30
|
+
costs an unknown amount, not $0.
|
|
31
|
+
|
|
32
|
+
Everything else (a ``provider/model`` string, a fine-tune ``ft:`` id, a
|
|
33
|
+
``tiered_pricing`` table, a model the table doesn't hold) is unpriced:
|
|
34
|
+
:func:`cost_per_token` raises :class:`UnpriceableModelError` and
|
|
35
|
+
:func:`priced_cost_per_token` returns ``None``.
|
|
36
|
+
|
|
37
|
+
LOCAL OVERRIDES. A few bare ids litellm doesn't price yet are priced from the
|
|
38
|
+
vendor's published rates. They live in ``scripts/update_prices.py``, which
|
|
39
|
+
writes them into the table with an :data:`OVERRIDE_MARKER` block citing the
|
|
40
|
+
source, and drops each one the day upstream prices the same key. A session
|
|
41
|
+
priced from one is labeled :data:`COST_SOURCE_WITH_OVERRIDES`.
|
|
42
|
+
|
|
43
|
+
THE ARITHMETIC, as litellm 1.103.2 performs it for these providers
|
|
44
|
+
(``cost_calculator.cost_per_token`` -> ``generic_cost_per_token``)::
|
|
45
|
+
|
|
46
|
+
text = max(prompt_tokens - cache_read - cache_creation, 0)
|
|
47
|
+
prompt_cost = float(text) * input_rate
|
|
48
|
+
prompt_cost += float(cache_read) * cache_read_rate
|
|
49
|
+
if cache_creation:
|
|
50
|
+
writing = 0.0
|
|
51
|
+
writing += cache_creation * cache_creation_rate
|
|
52
|
+
prompt_cost += writing
|
|
53
|
+
completion_cost = float(completion_tokens) * output_rate
|
|
54
|
+
|
|
55
|
+
where the four rates come from ``_get_token_base_cost``: the service-tier
|
|
56
|
+
variant of each key when the tier is ``flex`` / ``priority`` / ``fast`` /
|
|
57
|
+
``ultrafast`` (``fast`` reads the ``_priority`` key; ``standard`` and any
|
|
58
|
+
other tier read the base key), then the ``*_above_<n>k_tokens`` variants when
|
|
59
|
+
``prompt_tokens`` (input tokens alone, cache excluded) exceeds the largest
|
|
60
|
+
threshold the entry prices. A missing input or output rate is ``0.0``, and a
|
|
61
|
+
missing service-tier key falls back to its base key. A missing cache-creation
|
|
62
|
+
or cache-read rate (after the tier and threshold lookups) is the resolved input
|
|
63
|
+
rate, since a provider that publishes no cache price bills cached tokens as
|
|
64
|
+
ordinary input; an explicit ``0.0`` stays a real price. A missing one-hour
|
|
65
|
+
cache-write rate is the resolved cache-creation rate. litellm took that rule in
|
|
66
|
+
v1.103.0 (BerriAI/litellm commits 22b377fe2aad, "price cache writes without a
|
|
67
|
+
creation rate", 8573241c4981, "resolve a missing 1h cache write rate after
|
|
68
|
+
off-peak pricing", and 88d1eb3b35b4, "bill cache-read tokens at the input rate
|
|
69
|
+
when the map has no cache-read rate"); before it, both cache rates defaulted to
|
|
70
|
+
``0.0``.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
from __future__ import annotations
|
|
74
|
+
|
|
75
|
+
import json
|
|
76
|
+
import re
|
|
77
|
+
from dataclasses import dataclass
|
|
78
|
+
from functools import cache
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
from typing import TYPE_CHECKING, Any, Final, NoReturn
|
|
81
|
+
|
|
82
|
+
from loguru import logger
|
|
83
|
+
|
|
84
|
+
if TYPE_CHECKING:
|
|
85
|
+
from collections.abc import Iterable
|
|
86
|
+
|
|
87
|
+
#: The vendored price table, beside this module (``scripts/update_prices.py`` writes it).
|
|
88
|
+
TABLE_FILENAME: Final = "model_prices.json"
|
|
89
|
+
TABLE_PATH: Final = Path(__file__).with_name(TABLE_FILENAME)
|
|
90
|
+
|
|
91
|
+
#: The block ``scripts/update_prices.py`` stamps on a locally priced entry.
|
|
92
|
+
OVERRIDE_MARKER: Final = "atif_sql_override"
|
|
93
|
+
|
|
94
|
+
#: Providers whose ``cost_per_token`` branch is plain ``generic_cost_per_token``.
|
|
95
|
+
_COVERED_PROVIDERS: Final = frozenset({"anthropic", "openai", "bedrock"})
|
|
96
|
+
|
|
97
|
+
#: ``litellm.constants.REPLICATE_MODEL_NAME_WITH_ID_LENGTH`` (default 64): a
|
|
98
|
+
#: model with a ``:`` and a longer name than this is checked as a Replicate id
|
|
99
|
+
#: before the Bedrock membership test is reached.
|
|
100
|
+
_REPLICATE_ID_LENGTH: Final = 64
|
|
101
|
+
|
|
102
|
+
#: ``_SERVICE_TIER_SUFFIXES``: ``ServiceTier`` values as key suffixes, longest
|
|
103
|
+
#: first (a stable sort keeps the enum order among equal lengths).
|
|
104
|
+
_SERVICE_TIER_SUFFIXES: Final = ("_ultrafast", "_priority", "_auto", "_flex", "_fast")
|
|
105
|
+
|
|
106
|
+
#: ``_SERVICE_TIER_TO_COST_KEY_SUFFIX``.
|
|
107
|
+
_SERVICE_TIER_TO_COST_KEY_SUFFIX: Final = {
|
|
108
|
+
"flex": "flex",
|
|
109
|
+
"priority": "priority",
|
|
110
|
+
"fast": "priority",
|
|
111
|
+
"ultrafast": "ultrafast",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#: ``_ABOVE_THRESHOLD_COST_KEY``: an entry key with this tail is copied into
|
|
115
|
+
#: litellm's ``ModelInfoBase`` even when it is not one of the named fields.
|
|
116
|
+
_ABOVE_THRESHOLD_KEY: Final = re.compile(r"_above_\d+k?_tokens$")
|
|
117
|
+
|
|
118
|
+
#: The ``ModelInfoBase`` fields ``_get_token_base_cost`` and
|
|
119
|
+
#: ``_calculate_input_cost`` can read on our path. A key outside this set and
|
|
120
|
+
#: not matching :data:`_ABOVE_THRESHOLD_KEY` reads as ``None`` in litellm even
|
|
121
|
+
#: when the raw entry carries it, so the same must hold here.
|
|
122
|
+
_MODEL_INFO_FIELDS: Final = frozenset(
|
|
123
|
+
{
|
|
124
|
+
"input_cost_per_token",
|
|
125
|
+
"input_cost_per_token_flex",
|
|
126
|
+
"input_cost_per_token_priority",
|
|
127
|
+
"input_cost_per_token_ultrafast",
|
|
128
|
+
"input_cost_per_token_above_200k_tokens_priority",
|
|
129
|
+
"input_cost_per_token_above_272k_tokens_priority",
|
|
130
|
+
"input_cost_per_token_above_272k_tokens_flex",
|
|
131
|
+
"output_cost_per_token",
|
|
132
|
+
"output_cost_per_token_flex",
|
|
133
|
+
"output_cost_per_token_priority",
|
|
134
|
+
"output_cost_per_token_ultrafast",
|
|
135
|
+
"output_cost_per_token_above_200k_tokens_priority",
|
|
136
|
+
"output_cost_per_token_above_272k_tokens_priority",
|
|
137
|
+
"output_cost_per_token_above_272k_tokens_flex",
|
|
138
|
+
"output_cost_per_image_token",
|
|
139
|
+
"cache_creation_input_token_cost",
|
|
140
|
+
"cache_creation_input_token_cost_flex",
|
|
141
|
+
"cache_creation_input_token_cost_priority",
|
|
142
|
+
"cache_creation_input_token_cost_ultrafast",
|
|
143
|
+
"cache_creation_input_token_cost_above_1hr",
|
|
144
|
+
"cache_creation_input_token_cost_above_272k_tokens_priority",
|
|
145
|
+
"cache_creation_input_token_cost_above_272k_tokens_flex",
|
|
146
|
+
"cache_read_input_token_cost",
|
|
147
|
+
"cache_read_input_token_cost_flex",
|
|
148
|
+
"cache_read_input_token_cost_priority",
|
|
149
|
+
"cache_read_input_token_cost_ultrafast",
|
|
150
|
+
"cache_read_input_token_cost_above_200k_tokens_priority",
|
|
151
|
+
"cache_read_input_token_cost_above_272k_tokens_priority",
|
|
152
|
+
"cache_read_input_token_cost_above_272k_tokens_flex",
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
#: ``ModelInfoBase`` names the ``input_cost_per_token_above_*`` fields below
|
|
157
|
+
#: whether or not the entry prices them; they take part in the threshold scan
|
|
158
|
+
#: (with ``None`` values, which the scan skips) exactly like a raw key would.
|
|
159
|
+
_NAMED_THRESHOLD_KEYS: Final = (
|
|
160
|
+
"input_cost_per_token_above_128k_tokens",
|
|
161
|
+
"input_cost_per_token_above_200k_tokens",
|
|
162
|
+
"input_cost_per_token_above_272k_tokens",
|
|
163
|
+
"input_cost_per_token_above_512k_tokens",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
_ANTHROPIC_TEXT_MODELS: Final = frozenset({"claude-2", "claude-instant-1"})
|
|
167
|
+
_OPENAI_HARDCODED: Final = frozenset({"dall-e-2", "dall-e-3", "sora-2"})
|
|
168
|
+
|
|
169
|
+
#: ``litellm.azure_llms`` remaps these bare names to ``azure/...`` keys before lookup.
|
|
170
|
+
_AZURE_REMAPPED: Final = frozenset({"gpt-35-turbo", "gpt-35-turbo-16k", "gpt-35-turbo-instruct"})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class FastPathUnsupported(Exception): # noqa: N818, a routing signal rather than an error
|
|
174
|
+
"""The fast path does not replicate litellm for this call, so the call is unpriced."""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _decline(reason: str) -> NoReturn:
|
|
178
|
+
raise FastPathUnsupported(reason)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class UnpriceableModelError(ValueError):
|
|
182
|
+
"""No price for this model, so the caller records no cost."""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True, slots=True)
|
|
186
|
+
class _CapabilityRule:
|
|
187
|
+
pattern: re.Pattern[str]
|
|
188
|
+
model_info: dict[str, Any]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True, slots=True)
|
|
192
|
+
class _RoutingRule:
|
|
193
|
+
pattern: re.Pattern[str]
|
|
194
|
+
provider: str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True, slots=True)
|
|
198
|
+
class PricingTable:
|
|
199
|
+
"""The vendored price table as ``litellm.model_cost`` would hold it, plus its fallback rules."""
|
|
200
|
+
|
|
201
|
+
path: Path
|
|
202
|
+
#: The litellm git ref the table was generated from (``meta.ref``).
|
|
203
|
+
source_ref: str
|
|
204
|
+
entries: dict[str, dict[str, Any]]
|
|
205
|
+
lowercase_keys: dict[str, str]
|
|
206
|
+
routing_rules: tuple[_RoutingRule, ...]
|
|
207
|
+
capability_rules: tuple[_CapabilityRule, ...]
|
|
208
|
+
|
|
209
|
+
def lookup_key(self, name: str) -> str | None:
|
|
210
|
+
"""``litellm.utils._get_model_cost_key``: exact match, then case-insensitive."""
|
|
211
|
+
if name in self.entries:
|
|
212
|
+
return name
|
|
213
|
+
return self.lowercase_keys.get(name.lower())
|
|
214
|
+
|
|
215
|
+
def match_routing(self, model: str) -> str | None:
|
|
216
|
+
"""The provider of the first routing rule whose regex matches ``model``."""
|
|
217
|
+
if not model:
|
|
218
|
+
return None
|
|
219
|
+
return next(
|
|
220
|
+
(rule.provider for rule in self.routing_rules if rule.pattern.search(model)),
|
|
221
|
+
None,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def match_capabilities(self, model: str) -> dict[str, Any] | None:
|
|
225
|
+
"""The union, in file order, of every capability rule matching ``model``."""
|
|
226
|
+
if not model:
|
|
227
|
+
return None
|
|
228
|
+
matched = [rule.model_info for rule in self.capability_rules if rule.pattern.search(model)]
|
|
229
|
+
if not matched:
|
|
230
|
+
return None
|
|
231
|
+
return {key: value for info in matched for key, value in info.items()}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass(frozen=True, slots=True)
|
|
235
|
+
class _Rates:
|
|
236
|
+
"""The rates ``_get_token_base_cost`` resolves; a cache rate is ``None`` until resolved."""
|
|
237
|
+
|
|
238
|
+
prompt: float
|
|
239
|
+
completion: float
|
|
240
|
+
cache_creation: float | None
|
|
241
|
+
cache_creation_above_1hr: float | None
|
|
242
|
+
cache_read: float | None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass(frozen=True, slots=True)
|
|
246
|
+
class _ResolvedRates:
|
|
247
|
+
"""The five rates ``_get_token_base_cost`` returns, every one a float."""
|
|
248
|
+
|
|
249
|
+
prompt: float
|
|
250
|
+
completion: float
|
|
251
|
+
cache_creation: float
|
|
252
|
+
cache_creation_above_1hr: float
|
|
253
|
+
cache_read: float
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# --------------------------------------------------------------------------- table loading
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _resolve_legacy_extends(rules: list[Any]) -> list[Any]:
|
|
260
|
+
"""``fallback_generalizations._resolve_legacy_extends``: single-level ``extends``."""
|
|
261
|
+
base_by_name: dict[str, dict[str, Any]] = {
|
|
262
|
+
rule["name"]: rule["model_info"]
|
|
263
|
+
for rule in rules
|
|
264
|
+
if isinstance(rule, dict)
|
|
265
|
+
and isinstance(rule.get("name"), str)
|
|
266
|
+
and isinstance(rule.get("model_info"), dict)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
def resolved(rule: Any) -> Any:
|
|
270
|
+
if not isinstance(rule, dict):
|
|
271
|
+
return rule
|
|
272
|
+
parent_name = rule.get("extends")
|
|
273
|
+
own_info = rule.get("model_info")
|
|
274
|
+
parent_info = base_by_name.get(parent_name) if isinstance(parent_name, str) else None
|
|
275
|
+
if parent_info is None or not isinstance(own_info, dict):
|
|
276
|
+
return rule
|
|
277
|
+
return {**rule, "model_info": {**parent_info, **own_info}}
|
|
278
|
+
|
|
279
|
+
return [resolved(rule) for rule in rules]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _compile_rules(raw_rules: Any) -> tuple[tuple[_RoutingRule, ...], tuple[_CapabilityRule, ...]]:
|
|
283
|
+
"""``fallback_generalizations._compile_rule`` over the whole list; malformed rules are skipped."""
|
|
284
|
+
routing: list[_RoutingRule] = []
|
|
285
|
+
capability: list[_CapabilityRule] = []
|
|
286
|
+
installed = raw_rules if isinstance(raw_rules, list) else []
|
|
287
|
+
for rule in _resolve_legacy_extends(installed):
|
|
288
|
+
if not isinstance(rule, dict):
|
|
289
|
+
continue
|
|
290
|
+
pattern = rule.get("pattern")
|
|
291
|
+
model_info = rule.get("model_info")
|
|
292
|
+
if not isinstance(pattern, str) or not isinstance(model_info, dict):
|
|
293
|
+
continue
|
|
294
|
+
try:
|
|
295
|
+
compiled = re.compile(pattern, re.IGNORECASE)
|
|
296
|
+
except re.error:
|
|
297
|
+
continue
|
|
298
|
+
if "litellm_provider" not in model_info:
|
|
299
|
+
capability.append(_CapabilityRule(compiled, model_info))
|
|
300
|
+
continue
|
|
301
|
+
provider = model_info["litellm_provider"]
|
|
302
|
+
if not isinstance(provider, str):
|
|
303
|
+
continue
|
|
304
|
+
routing.append(_RoutingRule(compiled, provider))
|
|
305
|
+
if len(model_info) > 1:
|
|
306
|
+
capability.append(_CapabilityRule(compiled, model_info))
|
|
307
|
+
return tuple(routing), tuple(capability)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _expand_model_aliases(entries: dict[str, dict[str, Any]]) -> None:
|
|
311
|
+
"""``get_model_cost_map._expand_model_aliases``: alias keys share the canonical dict."""
|
|
312
|
+
aliases_to_add: dict[str, dict[str, Any]] = {}
|
|
313
|
+
keys_with_aliases: list[str] = []
|
|
314
|
+
for model_name, model_info in entries.items():
|
|
315
|
+
aliases = model_info.get("aliases")
|
|
316
|
+
if aliases is None:
|
|
317
|
+
continue
|
|
318
|
+
keys_with_aliases.append(model_name)
|
|
319
|
+
if not isinstance(aliases, list) or not aliases:
|
|
320
|
+
continue
|
|
321
|
+
for alias in aliases:
|
|
322
|
+
if alias in entries or alias in aliases_to_add:
|
|
323
|
+
continue
|
|
324
|
+
aliases_to_add[alias] = model_info
|
|
325
|
+
for key in keys_with_aliases:
|
|
326
|
+
entries[key].pop("aliases", None)
|
|
327
|
+
entries.update(aliases_to_add)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@cache
|
|
331
|
+
def load_table() -> PricingTable | None:
|
|
332
|
+
"""Parse the vendored table once per process; ``None`` (with a warning) when it's unreadable."""
|
|
333
|
+
try:
|
|
334
|
+
raw = json.loads(TABLE_PATH.read_text(encoding="utf-8"))
|
|
335
|
+
except (OSError, ValueError) as exc:
|
|
336
|
+
logger.warning("Price table {} is unreadable ({}); costs stay unpriced", TABLE_PATH, exc)
|
|
337
|
+
return None
|
|
338
|
+
models = raw.get("models") if isinstance(raw, dict) else None
|
|
339
|
+
if not isinstance(models, dict):
|
|
340
|
+
logger.warning("Price table {} has no models object; costs stay unpriced", TABLE_PATH)
|
|
341
|
+
return None
|
|
342
|
+
meta = raw.get("meta")
|
|
343
|
+
source_ref = meta.get("ref") if isinstance(meta, dict) else None
|
|
344
|
+
generalizations = raw.get("fallback_generalizations")
|
|
345
|
+
rules = generalizations.get("rules") if isinstance(generalizations, dict) else None
|
|
346
|
+
routing, capability = _compile_rules(rules)
|
|
347
|
+
entries: dict[str, dict[str, Any]] = {k: v for k, v in models.items() if isinstance(v, dict)}
|
|
348
|
+
_expand_model_aliases(entries)
|
|
349
|
+
return PricingTable(
|
|
350
|
+
path=TABLE_PATH,
|
|
351
|
+
source_ref=source_ref if isinstance(source_ref, str) else "unknown",
|
|
352
|
+
entries=entries,
|
|
353
|
+
lowercase_keys={k.lower(): k for k in entries},
|
|
354
|
+
routing_rules=routing,
|
|
355
|
+
capability_rules=capability,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# --------------------------------------------------------------------------- resolution
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _infer_provider(table: PricingTable, model: str, entry: dict[str, Any] | None) -> str:
|
|
363
|
+
"""``get_llm_provider`` for a bare model name, restricted to the covered providers.
|
|
364
|
+
|
|
365
|
+
The order is litellm's: the OpenAI membership tests, then Anthropic, then
|
|
366
|
+
the Replicate id-length test that shadows the Bedrock test, then Bedrock,
|
|
367
|
+
then the table's routing rules as the last resort for an unmapped name.
|
|
368
|
+
"""
|
|
369
|
+
if "ft:" in model or model in _AZURE_REMAPPED:
|
|
370
|
+
_decline("fine-tune id or azure alias")
|
|
371
|
+
if entry is None:
|
|
372
|
+
return _infer_unmapped_provider(table, model)
|
|
373
|
+
litellm_provider = entry.get("litellm_provider")
|
|
374
|
+
if litellm_provider == "openai" or model in _OPENAI_HARDCODED or model.startswith("gpt-image"):
|
|
375
|
+
if litellm_provider != "openai":
|
|
376
|
+
_decline("hardcoded OpenAI name under another provider")
|
|
377
|
+
return "openai"
|
|
378
|
+
if litellm_provider == "anthropic":
|
|
379
|
+
if model in _ANTHROPIC_TEXT_MODELS:
|
|
380
|
+
_decline("anthropic_text model")
|
|
381
|
+
return "anthropic"
|
|
382
|
+
if litellm_provider not in {"bedrock", "bedrock_converse"}:
|
|
383
|
+
_decline(f"provider {litellm_provider!r}")
|
|
384
|
+
if ":" in model and len(model) > _REPLICATE_ID_LENGTH:
|
|
385
|
+
# litellm takes the Replicate branch, sets nothing, and lands on the routing rules.
|
|
386
|
+
if table.match_routing(model) != "bedrock":
|
|
387
|
+
_decline("long Bedrock id not routed back to bedrock")
|
|
388
|
+
return "bedrock"
|
|
389
|
+
if litellm_provider == "bedrock" and entry.get("mode") == "guardrail":
|
|
390
|
+
_decline("bedrock guardrail")
|
|
391
|
+
return "bedrock"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _infer_unmapped_provider(table: PricingTable, model: str) -> str:
|
|
395
|
+
"""The routing-rule tail of ``get_llm_provider`` for a name the table does not hold."""
|
|
396
|
+
if (
|
|
397
|
+
model in _OPENAI_HARDCODED
|
|
398
|
+
or model.startswith(("gpt-image", "amazon_nova"))
|
|
399
|
+
or model == "*"
|
|
400
|
+
or (":" in model and len(model) > _REPLICATE_ID_LENGTH)
|
|
401
|
+
):
|
|
402
|
+
_decline("unmapped name with a hardcoded provider rule")
|
|
403
|
+
routed = table.match_routing(model)
|
|
404
|
+
if routed is None:
|
|
405
|
+
raise UnpriceableModelError(model)
|
|
406
|
+
if routed != "anthropic":
|
|
407
|
+
_decline(f"unmapped model routed to {routed!r}")
|
|
408
|
+
return routed
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _provider_matches(entry: dict[str, Any], provider: str) -> bool:
|
|
412
|
+
"""``litellm.utils._check_provider_match`` for the covered providers."""
|
|
413
|
+
litellm_provider = entry.get("litellm_provider")
|
|
414
|
+
if litellm_provider is None or litellm_provider == provider:
|
|
415
|
+
return True
|
|
416
|
+
return provider.startswith("bedrock") and str(litellm_provider).startswith("bedrock")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _resolve(table: PricingTable, model: str) -> dict[str, Any]:
|
|
420
|
+
"""``get_model_info``'s entry for ``model``: the raw dict litellm builds ``ModelInfoBase`` from."""
|
|
421
|
+
entry = table.entries.get(model)
|
|
422
|
+
provider = _infer_provider(table, model, entry)
|
|
423
|
+
prefixed = table.lookup_key(f"{provider}/{model}")
|
|
424
|
+
if prefixed is not None and _provider_matches(table.entries[prefixed], provider):
|
|
425
|
+
return table.entries[prefixed]
|
|
426
|
+
if entry is not None:
|
|
427
|
+
return entry
|
|
428
|
+
if table.lookup_key(model) is not None:
|
|
429
|
+
_decline("key differs from the model name by case")
|
|
430
|
+
# An unmapped bare Claude id: litellm builds the entry from the capability rules.
|
|
431
|
+
candidates = (f"{provider}/{model}", model)
|
|
432
|
+
if any(table.lookup_key(candidate) is not None for candidate in candidates):
|
|
433
|
+
_decline("a stripped candidate is a table key")
|
|
434
|
+
for candidate in candidates:
|
|
435
|
+
generalized = table.match_capabilities(candidate)
|
|
436
|
+
if generalized is not None:
|
|
437
|
+
return {**generalized, "litellm_provider": provider}
|
|
438
|
+
raise UnpriceableModelError(model)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# --------------------------------------------------------------------------- rates
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _info_get(entry: dict[str, Any], key: str) -> Any:
|
|
445
|
+
"""``ModelInfoBase.get(key)`` for the dict litellm would have built from ``entry``."""
|
|
446
|
+
if key not in _MODEL_INFO_FIELDS and _ABOVE_THRESHOLD_KEY.search(key) is None:
|
|
447
|
+
return None
|
|
448
|
+
value = entry.get(key)
|
|
449
|
+
if value is None and key in {"input_cost_per_token", "output_cost_per_token"}:
|
|
450
|
+
return 0
|
|
451
|
+
return value
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _coerce_rate(value: Any) -> float | None:
|
|
455
|
+
if isinstance(value, float):
|
|
456
|
+
return value
|
|
457
|
+
if isinstance(value, int):
|
|
458
|
+
return float(value)
|
|
459
|
+
if isinstance(value, str):
|
|
460
|
+
try:
|
|
461
|
+
return float(value)
|
|
462
|
+
except ValueError:
|
|
463
|
+
return None
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _cost_per_unit(
|
|
468
|
+
entry: dict[str, Any], cost_key: str, default: float | None = 0.0
|
|
469
|
+
) -> float | None:
|
|
470
|
+
"""``_get_cost_per_unit``: the rate, else the base rate when a tier suffix is in the key."""
|
|
471
|
+
value = _info_get(entry, cost_key)
|
|
472
|
+
coerced = _coerce_rate(value)
|
|
473
|
+
if coerced is not None:
|
|
474
|
+
return coerced
|
|
475
|
+
if value is None:
|
|
476
|
+
for suffix in _SERVICE_TIER_SUFFIXES:
|
|
477
|
+
if suffix in cost_key:
|
|
478
|
+
fallback = _coerce_rate(_info_get(entry, cost_key.replace(suffix, "")))
|
|
479
|
+
if fallback is not None:
|
|
480
|
+
return fallback
|
|
481
|
+
break
|
|
482
|
+
return default
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _rate(entry: dict[str, Any], cost_key: str, default: float = 0.0) -> float:
|
|
486
|
+
""":func:`_cost_per_unit` with a float default, so the result is always a float."""
|
|
487
|
+
value = _cost_per_unit(entry, cost_key, default)
|
|
488
|
+
return default if value is None else value
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _tier_key(base_key: str, service_tier: str | None) -> str:
|
|
492
|
+
"""``_get_service_tier_cost_key``."""
|
|
493
|
+
if service_tier is None:
|
|
494
|
+
return base_key
|
|
495
|
+
suffix = _SERVICE_TIER_TO_COST_KEY_SUFFIX.get(service_tier.lower())
|
|
496
|
+
if suffix is None:
|
|
497
|
+
return base_key
|
|
498
|
+
return f"{base_key}_{suffix}"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _parse_threshold(key: str) -> float:
|
|
502
|
+
"""``_parse_above_token_threshold``: ``..._above_200k_tokens`` -> 200000.0."""
|
|
503
|
+
threshold_str = key.split("_above_")[1].split("_tokens", maxsplit=1)[0]
|
|
504
|
+
return float(threshold_str.replace("k", "")) * (1000 if "k" in threshold_str else 1)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _threshold_keys(entry: dict[str, Any]) -> list[str]:
|
|
508
|
+
"""The ``input_cost_per_token_above_*`` keys litellm's ``ModelInfoBase`` would carry."""
|
|
509
|
+
keys: list[str] = list(_NAMED_THRESHOLD_KEYS)
|
|
510
|
+
keys.extend(
|
|
511
|
+
key
|
|
512
|
+
for key in entry
|
|
513
|
+
if key.startswith("input_cost_per_token_above_")
|
|
514
|
+
and key not in keys
|
|
515
|
+
and _ABOVE_THRESHOLD_KEY.search(key)
|
|
516
|
+
)
|
|
517
|
+
return [key for key in keys if not key.endswith(_SERVICE_TIER_SUFFIXES)]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _tiered_rates(
|
|
521
|
+
entry: dict[str, Any], tail: str, service_tier: str | None, rates: _Rates
|
|
522
|
+
) -> _Rates:
|
|
523
|
+
"""Each rate's ``*<tail>`` variant (tier-suffixed when the tier is known), else itself."""
|
|
524
|
+
|
|
525
|
+
def tiered(base: str, current: float) -> float:
|
|
526
|
+
return _rate(entry, _tier_key(f"{base}{tail}", service_tier), current)
|
|
527
|
+
|
|
528
|
+
def tiered_cache(base: str, current: float | None) -> float | None:
|
|
529
|
+
# Default None: a cache rate stays unresolved through the threshold lookup.
|
|
530
|
+
return _cost_per_unit(entry, _tier_key(f"{base}{tail}", service_tier), current)
|
|
531
|
+
|
|
532
|
+
return _Rates(
|
|
533
|
+
prompt=tiered("input_cost_per_token", rates.prompt),
|
|
534
|
+
completion=tiered("output_cost_per_token", rates.completion),
|
|
535
|
+
cache_creation=tiered_cache("cache_creation_input_token_cost", rates.cache_creation),
|
|
536
|
+
cache_creation_above_1hr=tiered_cache(
|
|
537
|
+
"cache_creation_input_token_cost_above_1hr", rates.cache_creation_above_1hr
|
|
538
|
+
),
|
|
539
|
+
cache_read=tiered_cache("cache_read_input_token_cost", rates.cache_read),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _above_threshold_rates(
|
|
544
|
+
entry: dict[str, Any],
|
|
545
|
+
prompt_tokens: int,
|
|
546
|
+
service_tier: str | None,
|
|
547
|
+
rates: _Rates,
|
|
548
|
+
) -> _Rates:
|
|
549
|
+
"""The ``## CHECK IF ABOVE THRESHOLD`` block of ``_get_token_base_cost``."""
|
|
550
|
+
threshold_keys = _threshold_keys(entry)
|
|
551
|
+
if not threshold_keys:
|
|
552
|
+
return rates
|
|
553
|
+
for key in sorted(threshold_keys, key=_parse_threshold, reverse=True):
|
|
554
|
+
if _info_get(entry, key) is None:
|
|
555
|
+
continue
|
|
556
|
+
threshold_str = key.split("_above_")[1].split("_tokens")[0]
|
|
557
|
+
if not prompt_tokens > _parse_threshold(key):
|
|
558
|
+
continue
|
|
559
|
+
return _tiered_rates(entry, f"_above_{threshold_str}_tokens", service_tier, rates)
|
|
560
|
+
return rates
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _base_rates(
|
|
564
|
+
entry: dict[str, Any], prompt_tokens: int, service_tier: str | None
|
|
565
|
+
) -> _ResolvedRates:
|
|
566
|
+
"""``_get_token_base_cost`` for an entry without ``tiered_pricing``."""
|
|
567
|
+
tiered_pricing = entry.get("tiered_pricing")
|
|
568
|
+
if isinstance(tiered_pricing, list) and tiered_pricing:
|
|
569
|
+
_decline("tiered_pricing table")
|
|
570
|
+
if entry.get("off_peak_pricing"):
|
|
571
|
+
# litellm swaps in off-peak rates by wall-clock time; a conversion must not.
|
|
572
|
+
_decline("off_peak_pricing block")
|
|
573
|
+
prompt = _rate(entry, _tier_key("input_cost_per_token", service_tier))
|
|
574
|
+
completion = _rate(entry, _tier_key("output_cost_per_token", service_tier))
|
|
575
|
+
if completion == 0.0:
|
|
576
|
+
image_rate = _cost_per_unit(entry, "output_cost_per_image_token", None)
|
|
577
|
+
if image_rate is not None:
|
|
578
|
+
completion = image_rate
|
|
579
|
+
rates = _Rates(
|
|
580
|
+
prompt=prompt,
|
|
581
|
+
completion=completion,
|
|
582
|
+
cache_creation=_cost_per_unit(
|
|
583
|
+
entry, _tier_key("cache_creation_input_token_cost", service_tier), None
|
|
584
|
+
),
|
|
585
|
+
cache_creation_above_1hr=_cost_per_unit(
|
|
586
|
+
entry, "cache_creation_input_token_cost_above_1hr", None
|
|
587
|
+
),
|
|
588
|
+
cache_read=_cost_per_unit(
|
|
589
|
+
entry, _tier_key("cache_read_input_token_cost", service_tier), None
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
rates = _above_threshold_rates(entry, prompt_tokens, service_tier, rates)
|
|
593
|
+
# A missing cache rate bills as input (litellm v1.103.0, see the module docstring).
|
|
594
|
+
# An entry with off_peak_pricing was declined above, so the input rate is the resolved one.
|
|
595
|
+
cache_creation = rates.prompt if rates.cache_creation is None else rates.cache_creation
|
|
596
|
+
cache_read = rates.prompt if rates.cache_read is None else rates.cache_read
|
|
597
|
+
return _ResolvedRates(
|
|
598
|
+
prompt=rates.prompt,
|
|
599
|
+
completion=rates.completion,
|
|
600
|
+
cache_creation=cache_creation,
|
|
601
|
+
cache_creation_above_1hr=(
|
|
602
|
+
cache_creation
|
|
603
|
+
if rates.cache_creation_above_1hr is None
|
|
604
|
+
else rates.cache_creation_above_1hr
|
|
605
|
+
),
|
|
606
|
+
cache_read=cache_read,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# --------------------------------------------------------------------------- public API
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def fast_cost_per_token(
|
|
614
|
+
*,
|
|
615
|
+
model: str,
|
|
616
|
+
prompt_tokens: int,
|
|
617
|
+
completion_tokens: int,
|
|
618
|
+
cache_creation_input_tokens: int,
|
|
619
|
+
cache_read_input_tokens: int,
|
|
620
|
+
service_tier: str | None = None,
|
|
621
|
+
) -> tuple[float, float]:
|
|
622
|
+
"""``litellm.cost_per_token`` for the covered shapes, computed from the vendored table.
|
|
623
|
+
|
|
624
|
+
Raises :class:`FastPathUnsupported` for a shape this module does not
|
|
625
|
+
replicate and :class:`UnpriceableModelError` where litellm would raise.
|
|
626
|
+
"""
|
|
627
|
+
table = load_table()
|
|
628
|
+
if table is None:
|
|
629
|
+
_decline("price table unavailable")
|
|
630
|
+
if not model or "/" in model:
|
|
631
|
+
_decline("empty or provider-prefixed model")
|
|
632
|
+
counts = (
|
|
633
|
+
prompt_tokens,
|
|
634
|
+
completion_tokens,
|
|
635
|
+
cache_creation_input_tokens,
|
|
636
|
+
cache_read_input_tokens,
|
|
637
|
+
)
|
|
638
|
+
if any(type(count) is not int for count in counts):
|
|
639
|
+
_decline("non-int token counts")
|
|
640
|
+
entry = _resolve(table, model)
|
|
641
|
+
return _price_entry(
|
|
642
|
+
entry,
|
|
643
|
+
prompt_tokens=prompt_tokens,
|
|
644
|
+
completion_tokens=completion_tokens,
|
|
645
|
+
cache_creation_input_tokens=cache_creation_input_tokens,
|
|
646
|
+
cache_read_input_tokens=cache_read_input_tokens,
|
|
647
|
+
service_tier=service_tier,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _price_entry(
|
|
652
|
+
entry: dict[str, Any],
|
|
653
|
+
*,
|
|
654
|
+
prompt_tokens: int,
|
|
655
|
+
completion_tokens: int,
|
|
656
|
+
cache_creation_input_tokens: int,
|
|
657
|
+
cache_read_input_tokens: int,
|
|
658
|
+
service_tier: str | None,
|
|
659
|
+
) -> tuple[float, float]:
|
|
660
|
+
"""``generic_cost_per_token`` over one resolved entry (litellm's float order)."""
|
|
661
|
+
rates = _base_rates(entry, prompt_tokens, service_tier)
|
|
662
|
+
|
|
663
|
+
# generic_cost_per_token: the cache counts arrive on prompt_tokens_details, text
|
|
664
|
+
# tokens are unset, so both the double-counting branch and the clamp branch
|
|
665
|
+
# leave text = max(prompt - cache_read - cache_creation, 0).
|
|
666
|
+
text_tokens = max(prompt_tokens - cache_read_input_tokens - cache_creation_input_tokens, 0)
|
|
667
|
+
prompt_cost = float(text_tokens) * rates.prompt
|
|
668
|
+
prompt_cost += float(cache_read_input_tokens) * rates.cache_read
|
|
669
|
+
if cache_creation_input_tokens:
|
|
670
|
+
writing_cost = 0.0
|
|
671
|
+
writing_cost += cache_creation_input_tokens * rates.cache_creation
|
|
672
|
+
prompt_cost += writing_cost
|
|
673
|
+
completion_cost = float(completion_tokens) * rates.completion
|
|
674
|
+
return prompt_cost, completion_cost
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def cost_per_token(
|
|
678
|
+
*,
|
|
679
|
+
model: str,
|
|
680
|
+
prompt_tokens: int,
|
|
681
|
+
completion_tokens: int,
|
|
682
|
+
cache_creation_input_tokens: int,
|
|
683
|
+
cache_read_input_tokens: int,
|
|
684
|
+
service_tier: str | None = None,
|
|
685
|
+
) -> tuple[float, float]:
|
|
686
|
+
"""``(prompt_cost_usd, completion_cost_usd)`` from the vendored table.
|
|
687
|
+
|
|
688
|
+
Raises :class:`UnpriceableModelError` for a model or shape this module
|
|
689
|
+
can't price (litellm would raise, or the fast path doesn't replicate the
|
|
690
|
+
shape); the callers turn it into "no estimate", as harbor did.
|
|
691
|
+
"""
|
|
692
|
+
try:
|
|
693
|
+
return fast_cost_per_token(
|
|
694
|
+
model=model,
|
|
695
|
+
prompt_tokens=prompt_tokens,
|
|
696
|
+
completion_tokens=completion_tokens,
|
|
697
|
+
cache_creation_input_tokens=cache_creation_input_tokens,
|
|
698
|
+
cache_read_input_tokens=cache_read_input_tokens,
|
|
699
|
+
service_tier=service_tier,
|
|
700
|
+
)
|
|
701
|
+
except FastPathUnsupported as exc:
|
|
702
|
+
logger.debug("No price for model '{}' ({}); leaving it unpriced", model, exc)
|
|
703
|
+
raise UnpriceableModelError(model) from exc
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
#: The rate keys whose absence makes an entry "unpriced". A capability-rule
|
|
707
|
+
#: generalization (litellm's entry for an unmapped ``claude-*`` id) carries
|
|
708
|
+
#: neither.
|
|
709
|
+
_RATE_KEYS: Final = ("input_cost_per_token", "output_cost_per_token")
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def is_local_override(model: str | None) -> bool:
|
|
713
|
+
"""Whether ``model`` is priced from a local override rather than litellm's data."""
|
|
714
|
+
table = load_table()
|
|
715
|
+
if table is None or model is None:
|
|
716
|
+
return False
|
|
717
|
+
entry = table.entries.get(model)
|
|
718
|
+
return entry is not None and OVERRIDE_MARKER in entry
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def override_models() -> frozenset[str]:
|
|
722
|
+
"""Every model the vendored table prices from a local override."""
|
|
723
|
+
table = load_table()
|
|
724
|
+
overridden: set[str] = set()
|
|
725
|
+
if table is not None:
|
|
726
|
+
overridden.update(key for key, entry in table.entries.items() if OVERRIDE_MARKER in entry)
|
|
727
|
+
return frozenset(overridden)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _rate_less(entry: dict[str, Any]) -> bool:
|
|
731
|
+
return all(_coerce_rate(entry.get(key)) is None for key in _RATE_KEYS)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def priced_cost_per_token(
|
|
735
|
+
*,
|
|
736
|
+
model: str,
|
|
737
|
+
prompt_tokens: int,
|
|
738
|
+
completion_tokens: int,
|
|
739
|
+
cache_creation_input_tokens: int,
|
|
740
|
+
cache_read_input_tokens: int,
|
|
741
|
+
service_tier: str | None = None,
|
|
742
|
+
) -> tuple[float, float] | None:
|
|
743
|
+
"""The converter's pricing policy: a real price, or ``None`` when no one priced the model.
|
|
744
|
+
|
|
745
|
+
:func:`cost_per_token`, except that an entry with no rates at all (an
|
|
746
|
+
unmapped ``claude-*`` id litellm prices from its capability rules at
|
|
747
|
+
``(0.0, 0.0)``) and a model :func:`cost_per_token` can't price both come
|
|
748
|
+
back as ``None``.
|
|
749
|
+
"""
|
|
750
|
+
table = load_table()
|
|
751
|
+
if table is not None and model and "/" not in model:
|
|
752
|
+
try:
|
|
753
|
+
entry: dict[str, Any] | None = _resolve(table, model)
|
|
754
|
+
except (FastPathUnsupported, UnpriceableModelError):
|
|
755
|
+
entry = None
|
|
756
|
+
if entry is not None and _rate_less(entry):
|
|
757
|
+
logger.debug("No rates for model '{}'; leaving its cost unpriced", model)
|
|
758
|
+
return None
|
|
759
|
+
try:
|
|
760
|
+
return cost_per_token(
|
|
761
|
+
model=model,
|
|
762
|
+
prompt_tokens=prompt_tokens,
|
|
763
|
+
completion_tokens=completion_tokens,
|
|
764
|
+
cache_creation_input_tokens=cache_creation_input_tokens,
|
|
765
|
+
cache_read_input_tokens=cache_read_input_tokens,
|
|
766
|
+
service_tier=service_tier,
|
|
767
|
+
)
|
|
768
|
+
except UnpriceableModelError:
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
#: ``final_metrics.extra["cost_source"]`` for an estimate priced from litellm's data alone.
|
|
773
|
+
COST_SOURCE_LITELLM: Final = "litellm_estimate"
|
|
774
|
+
#: ... and for one where at least one step was priced from a local override.
|
|
775
|
+
COST_SOURCE_WITH_OVERRIDES: Final = "litellm_estimate+local_overrides"
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def cost_source_label(models: Iterable[str | None]) -> str:
|
|
779
|
+
"""Which data priced an estimate over steps of ``models``."""
|
|
780
|
+
if any(is_local_override(model) for model in models):
|
|
781
|
+
return COST_SOURCE_WITH_OVERRIDES
|
|
782
|
+
return COST_SOURCE_LITELLM
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def has_pricing_entry(key: str) -> bool:
|
|
786
|
+
"""``bool(litellm.model_cost.get(key))`` over the vendored table: it prices this exact key."""
|
|
787
|
+
table = load_table()
|
|
788
|
+
if table is None:
|
|
789
|
+
return False
|
|
790
|
+
return bool(table.entries.get(key))
|