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.
Files changed (164) hide show
  1. atif_analytics/__init__.py +9 -0
  2. atif_analytics/application/__init__.py +3 -0
  3. atif_analytics/application/analyze.py +158 -0
  4. atif_analytics/application/prompts.py +750 -0
  5. atif_analytics/application/use_cases/__init__.py +3 -0
  6. atif_analytics/application/use_cases/_shared.py +264 -0
  7. atif_analytics/application/use_cases/classify.py +430 -0
  8. atif_analytics/application/use_cases/conflicts.py +448 -0
  9. atif_analytics/application/use_cases/friction.py +634 -0
  10. atif_analytics/application/use_cases/perceived.py +451 -0
  11. atif_analytics/domain/__init__.py +3 -0
  12. atif_analytics/domain/authorship.py +166 -0
  13. atif_analytics/domain/config.py +30 -0
  14. atif_analytics/domain/costs.py +50 -0
  15. atif_analytics/domain/friction.py +117 -0
  16. atif_analytics/domain/layout.py +94 -0
  17. atif_analytics/domain/models.py +385 -0
  18. atif_analytics/domain/ports.py +100 -0
  19. atif_analytics/domain/transcript.py +359 -0
  20. atif_analytics/infrastructure/__init__.py +3 -0
  21. atif_analytics/infrastructure/corpus_reader.py +546 -0
  22. atif_analytics/infrastructure/parquet_cache.py +186 -0
  23. atif_analytics/infrastructure/settings.py +114 -0
  24. atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
  25. atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
  26. atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
  27. atif_analytics/py.typed +0 -0
  28. atif_cli/__init__.py +3 -0
  29. atif_cli/__main__.py +8 -0
  30. atif_cli/app.py +2459 -0
  31. atif_cli/converter_adapter.py +151 -0
  32. atif_cli/corpus.py +409 -0
  33. atif_cli/cron.py +311 -0
  34. atif_cli/duck_errors.py +97 -0
  35. atif_cli/embed_lake.py +101 -0
  36. atif_cli/errors.py +89 -0
  37. atif_cli/lake.py +373 -0
  38. atif_cli/lake_sessions.py +270 -0
  39. atif_cli/output.py +297 -0
  40. atif_cli/py.typed +0 -0
  41. atif_converter/__init__.py +3 -0
  42. atif_converter/application/__init__.py +3 -0
  43. atif_converter/application/convert_and_audit.py +294 -0
  44. atif_converter/application/convert_codex.py +210 -0
  45. atif_converter/domain/__init__.py +9 -0
  46. atif_converter/domain/agents.py +40 -0
  47. atif_converter/domain/atif/__init__.py +55 -0
  48. atif_converter/domain/atif/agent.py +37 -0
  49. atif_converter/domain/atif/content.py +161 -0
  50. atif_converter/domain/atif/final_metrics.py +45 -0
  51. atif_converter/domain/atif/metrics.py +49 -0
  52. atif_converter/domain/atif/observation.py +21 -0
  53. atif_converter/domain/atif/observation_result.py +47 -0
  54. atif_converter/domain/atif/step.py +144 -0
  55. atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
  56. atif_converter/domain/atif/tool_call.py +36 -0
  57. atif_converter/domain/atif/trajectory.py +211 -0
  58. atif_converter/domain/atif/trajectory_validator.py +299 -0
  59. atif_converter/domain/blobs.py +510 -0
  60. atif_converter/domain/claude_code_conversion.py +1077 -0
  61. atif_converter/domain/codex_conversion.py +1036 -0
  62. atif_converter/domain/codex_edges.py +192 -0
  63. atif_converter/domain/codex_enrichment.py +419 -0
  64. atif_converter/domain/codex_fidelity.py +124 -0
  65. atif_converter/domain/codex_nested_calls.py +165 -0
  66. atif_converter/domain/edges.py +113 -0
  67. atif_converter/domain/enrichment.py +412 -0
  68. atif_converter/domain/errors.py +60 -0
  69. atif_converter/domain/fidelity.py +171 -0
  70. atif_converter/domain/model_prices.json +8538 -0
  71. atif_converter/domain/pricing.py +790 -0
  72. atif_converter/domain/result_signals.py +774 -0
  73. atif_converter/domain/schema_version.py +90 -0
  74. atif_converter/domain/session_events.py +408 -0
  75. atif_converter/infrastructure/__init__.py +3 -0
  76. atif_converter/infrastructure/census.py +89 -0
  77. atif_converter/infrastructure/claude_code_converter.py +127 -0
  78. atif_converter/infrastructure/codex_adapter.py +132 -0
  79. atif_converter/infrastructure/codex_census.py +100 -0
  80. atif_converter/infrastructure/codex_converter.py +82 -0
  81. atif_converter/infrastructure/harbor_adapter.py +187 -0
  82. atif_converter/infrastructure/raw_records.py +426 -0
  83. atif_converter/infrastructure/source_archive.py +154 -0
  84. atif_converter/py.typed +0 -0
  85. atif_corpus/__init__.py +13 -0
  86. atif_corpus/application/__init__.py +7 -0
  87. atif_corpus/application/materialize.py +1826 -0
  88. atif_corpus/domain/__init__.py +9 -0
  89. atif_corpus/domain/agents.py +33 -0
  90. atif_corpus/domain/generation.py +34 -0
  91. atif_corpus/domain/layout.py +168 -0
  92. atif_corpus/domain/ports.py +175 -0
  93. atif_corpus/domain/session_id.py +68 -0
  94. atif_corpus/domain/sessions.py +228 -0
  95. atif_corpus/domain/slug.py +72 -0
  96. atif_corpus/domain/source_layout.py +143 -0
  97. atif_corpus/domain/watermark.py +76 -0
  98. atif_corpus/infrastructure/__init__.py +7 -0
  99. atif_corpus/infrastructure/atomic.py +266 -0
  100. atif_corpus/infrastructure/compress_artifacts.py +161 -0
  101. atif_corpus/infrastructure/fake_converter.py +174 -0
  102. atif_corpus/infrastructure/scanner.py +316 -0
  103. atif_corpus/infrastructure/settings.py +130 -0
  104. atif_corpus/infrastructure/source_archive.py +202 -0
  105. atif_corpus/py.typed +0 -0
  106. atif_duck/__init__.py +43 -0
  107. atif_duck/domain/__init__.py +32 -0
  108. atif_duck/domain/artifacts.py +66 -0
  109. atif_duck/domain/authorship.py +165 -0
  110. atif_duck/domain/catalog.py +626 -0
  111. atif_duck/domain/columnar.py +149 -0
  112. atif_duck/domain/embedding_guard.py +88 -0
  113. atif_duck/domain/examples.py +265 -0
  114. atif_duck/domain/lake.py +427 -0
  115. atif_duck/domain/lake_sessions.py +97 -0
  116. atif_duck/domain/raw_readers.py +140 -0
  117. atif_duck/domain/session_id.py +69 -0
  118. atif_duck/domain/sql_literal.py +50 -0
  119. atif_duck/infrastructure/__init__.py +34 -0
  120. atif_duck/infrastructure/analytics.py +469 -0
  121. atif_duck/infrastructure/authorship.py +139 -0
  122. atif_duck/infrastructure/columnar.py +641 -0
  123. atif_duck/infrastructure/lake.py +1383 -0
  124. atif_duck/infrastructure/lake_sessions.py +194 -0
  125. atif_duck/infrastructure/lake_settings.py +63 -0
  126. atif_duck/infrastructure/lake_steps.py +225 -0
  127. atif_duck/infrastructure/projections.py +221 -0
  128. atif_duck/infrastructure/registry.py +2194 -0
  129. atif_duck/infrastructure/stored_artifacts.py +115 -0
  130. atif_duck/py.typed +0 -0
  131. atif_embed/__init__.py +12 -0
  132. atif_embed/application/__init__.py +3 -0
  133. atif_embed/application/embed.py +293 -0
  134. atif_embed/application/prune.py +80 -0
  135. atif_embed/domain/__init__.py +3 -0
  136. atif_embed/domain/discovery.py +70 -0
  137. atif_embed/domain/embedding_guard.py +83 -0
  138. atif_embed/domain/errors.py +94 -0
  139. atif_embed/domain/ports.py +223 -0
  140. atif_embed/domain/sql_literal.py +49 -0
  141. atif_embed/domain/text_stamp.py +71 -0
  142. atif_embed/infrastructure/__init__.py +3 -0
  143. atif_embed/infrastructure/cohere_bedrock.py +433 -0
  144. atif_embed/infrastructure/corpus_text_rows.py +273 -0
  145. atif_embed/infrastructure/gemma_local.py +355 -0
  146. atif_embed/infrastructure/lake_text_rows.py +203 -0
  147. atif_embed/infrastructure/lance_store.py +440 -0
  148. atif_embed/infrastructure/providers.py +30 -0
  149. atif_embed/infrastructure/settings.py +154 -0
  150. atif_embed/py.typed +0 -0
  151. atif_models/__init__.py +14 -0
  152. atif_models/domain/__init__.py +9 -0
  153. atif_models/domain/ports.py +143 -0
  154. atif_models/domain/registry.py +151 -0
  155. atif_models/domain/schema.py +109 -0
  156. atif_models/infrastructure/__init__.py +8 -0
  157. atif_models/infrastructure/openai_bedrock.py +314 -0
  158. atif_models/infrastructure/settings.py +85 -0
  159. atif_models/py.typed +0 -0
  160. atif_sql-0.2.0.dist-info/METADATA +272 -0
  161. atif_sql-0.2.0.dist-info/RECORD +164 -0
  162. atif_sql-0.2.0.dist-info/WHEEL +4 -0
  163. atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
  164. 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))