codex-usage-tracking 0.3.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 (50) hide show
  1. codex_usage_tracker/__init__.py +7 -0
  2. codex_usage_tracker/__main__.py +6 -0
  3. codex_usage_tracker/allowance.py +759 -0
  4. codex_usage_tracker/api_payloads.py +90 -0
  5. codex_usage_tracker/cli.py +1326 -0
  6. codex_usage_tracker/context.py +410 -0
  7. codex_usage_tracker/costing.py +176 -0
  8. codex_usage_tracker/dashboard.py +389 -0
  9. codex_usage_tracker/diagnostics.py +624 -0
  10. codex_usage_tracker/formatting.py +225 -0
  11. codex_usage_tracker/json_contracts.py +350 -0
  12. codex_usage_tracker/mcp_server.py +371 -0
  13. codex_usage_tracker/models.py +92 -0
  14. codex_usage_tracker/parser.py +491 -0
  15. codex_usage_tracker/paths.py +18 -0
  16. codex_usage_tracker/plugin_data/__init__.py +1 -0
  17. codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
  18. codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
  19. codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
  20. codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
  21. codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
  22. codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
  23. codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
  24. codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
  25. codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
  26. codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
  27. codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
  28. codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
  29. codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
  30. codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
  31. codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
  32. codex_usage_tracker/plugin_installer.py +312 -0
  33. codex_usage_tracker/pricing.py +57 -0
  34. codex_usage_tracker/pricing_config.py +223 -0
  35. codex_usage_tracker/pricing_estimates.py +44 -0
  36. codex_usage_tracker/pricing_openai.py +253 -0
  37. codex_usage_tracker/projects.py +347 -0
  38. codex_usage_tracker/recommendations.py +270 -0
  39. codex_usage_tracker/reports.py +637 -0
  40. codex_usage_tracker/schema.py +71 -0
  41. codex_usage_tracker/server.py +400 -0
  42. codex_usage_tracker/store.py +666 -0
  43. codex_usage_tracker/support.py +147 -0
  44. codex_usage_tracker/threads.py +183 -0
  45. codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
  46. codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
  47. codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
  48. codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
  49. codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
  50. codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,57 @@
1
+ """Public pricing facade for config, source parsing, estimates, and costing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codex_usage_tracker.costing import (
6
+ annotate_rows_with_efficiency,
7
+ efficiency_flags,
8
+ estimate_cache_savings_usd,
9
+ estimate_cost_usd,
10
+ summarize_pricing_coverage,
11
+ )
12
+ from codex_usage_tracker.pricing_config import (
13
+ PRICING_SCHEMA,
14
+ PRICING_TEMPLATE,
15
+ PricingConfig,
16
+ load_pricing_config,
17
+ pin_pricing_snapshot,
18
+ write_pricing_template,
19
+ )
20
+ from codex_usage_tracker.pricing_estimates import (
21
+ ESTIMATED_MODEL_PRICES,
22
+ OPENAI_CODEX_LAUNCH_URL,
23
+ OPENAI_CODEX_RATE_CARD_URL,
24
+ OPENAI_GPT_53_CODEX_MODEL_URL,
25
+ )
26
+ from codex_usage_tracker.pricing_openai import (
27
+ OPENAI_PRICING_MD_URL,
28
+ VALID_PRICING_TIERS,
29
+ PricingParseError,
30
+ PricingUpdateResult,
31
+ parse_openai_pricing_markdown,
32
+ update_pricing_from_openai_docs,
33
+ )
34
+
35
+ __all__ = [
36
+ "ESTIMATED_MODEL_PRICES",
37
+ "OPENAI_CODEX_LAUNCH_URL",
38
+ "OPENAI_CODEX_RATE_CARD_URL",
39
+ "OPENAI_GPT_53_CODEX_MODEL_URL",
40
+ "OPENAI_PRICING_MD_URL",
41
+ "PRICING_SCHEMA",
42
+ "PRICING_TEMPLATE",
43
+ "VALID_PRICING_TIERS",
44
+ "PricingConfig",
45
+ "PricingParseError",
46
+ "PricingUpdateResult",
47
+ "annotate_rows_with_efficiency",
48
+ "efficiency_flags",
49
+ "estimate_cache_savings_usd",
50
+ "estimate_cost_usd",
51
+ "load_pricing_config",
52
+ "parse_openai_pricing_markdown",
53
+ "pin_pricing_snapshot",
54
+ "summarize_pricing_coverage",
55
+ "update_pricing_from_openai_docs",
56
+ "write_pricing_template",
57
+ ]
@@ -0,0 +1,223 @@
1
+ """Local pricing config loading and template writing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from codex_usage_tracker.paths import DEFAULT_PRICING_PATH
12
+
13
+ PRICING_SCHEMA = "codex-usage-tracker-pricing-v1"
14
+ PRICING_TEMPLATE = {
15
+ "_comment": (
16
+ "Fill in current prices in USD per 1 million tokens. The tracker does "
17
+ "not fetch pricing during normal reports. Prefer update-pricing when "
18
+ "you want to cache current OpenAI-published rates locally."
19
+ ),
20
+ "models": {
21
+ "replace-with-model-name": {
22
+ "input_per_million": 0.0,
23
+ "cached_input_per_million": 0.0,
24
+ "output_per_million": 0.0,
25
+ }
26
+ },
27
+ "aliases": {
28
+ "local-codex-model-label": "official-openai-model-id",
29
+ },
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class PricingConfig:
35
+ """Parsed local model pricing config."""
36
+
37
+ path: Path
38
+ models: dict[str, dict[str, float]]
39
+ loaded: bool
40
+ aliases: dict[str, str] | None = None
41
+ estimated_models: set[str] | None = None
42
+ source: dict[str, Any] | None = None
43
+ error: str | None = None
44
+
45
+ def rates_for(self, model: object) -> dict[str, float] | None:
46
+ if not isinstance(model, str) or not model:
47
+ return None
48
+ direct = self.models.get(model)
49
+ if direct is not None:
50
+ return direct
51
+ alias_target = (self.aliases or {}).get(model)
52
+ if not alias_target:
53
+ return None
54
+ return self.models.get(alias_target)
55
+
56
+ def priced_as(self, model: object) -> str | None:
57
+ if not isinstance(model, str) or not model:
58
+ return None
59
+ if model in self.models:
60
+ return model
61
+ alias_target = (self.aliases or {}).get(model)
62
+ if alias_target and alias_target in self.models:
63
+ return alias_target
64
+ return None
65
+
66
+ def is_estimated_model(self, model: object) -> bool:
67
+ priced_as = self.priced_as(model)
68
+ return bool(priced_as and priced_as in (self.estimated_models or set()))
69
+
70
+
71
+ def load_pricing_config(path: Path = DEFAULT_PRICING_PATH) -> PricingConfig:
72
+ """Load optional local pricing without contacting external services."""
73
+
74
+ if not path.exists():
75
+ return PricingConfig(path=path, models={}, loaded=False)
76
+ try:
77
+ raw = json.loads(path.read_text(encoding="utf-8"))
78
+ models = parse_models(raw)
79
+ except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
80
+ return PricingConfig(path=path, models={}, loaded=False, error=str(exc))
81
+ source = raw.get("_source") if isinstance(raw, dict) else None
82
+ aliases = parse_aliases(raw)
83
+ return PricingConfig(
84
+ path=path,
85
+ models=models,
86
+ loaded=True,
87
+ aliases=aliases,
88
+ estimated_models=parse_estimated_models(raw),
89
+ source=source if isinstance(source, dict) else None,
90
+ )
91
+
92
+
93
+ def write_pricing_template(path: Path = DEFAULT_PRICING_PATH, force: bool = False) -> Path:
94
+ """Write a local pricing template for user-maintained cost estimates."""
95
+
96
+ if path.exists() and not force:
97
+ raise FileExistsError(f"Pricing config already exists: {path}")
98
+ path.parent.mkdir(parents=True, exist_ok=True)
99
+ path.write_text(json.dumps(PRICING_TEMPLATE, indent=2) + "\n", encoding="utf-8")
100
+ return path
101
+
102
+
103
+ def pin_pricing_snapshot(
104
+ *,
105
+ source_path: Path = DEFAULT_PRICING_PATH,
106
+ output_path: Path,
107
+ force: bool = False,
108
+ ) -> Path:
109
+ """Copy the current local pricing config to a reproducible report snapshot."""
110
+
111
+ config = load_pricing_config(source_path)
112
+ if config.error:
113
+ raise ValueError(f"pricing config is invalid: {config.error}")
114
+ if not config.loaded:
115
+ raise FileNotFoundError(f"pricing config does not exist: {source_path}")
116
+ output_path = output_path.expanduser()
117
+ if output_path.exists() and not force:
118
+ raise FileExistsError(f"pricing snapshot already exists: {output_path}")
119
+ raw = json.loads(source_path.expanduser().read_text(encoding="utf-8"))
120
+ if not isinstance(raw, dict):
121
+ raise ValueError("pricing config must be a JSON object")
122
+ source_payload = raw.get("_source")
123
+ source: dict[str, Any] = source_payload if isinstance(source_payload, dict) else {}
124
+ raw["_source"] = {
125
+ **source,
126
+ "pinned": True,
127
+ "pinned_at": datetime.now(timezone.utc)
128
+ .replace(microsecond=0)
129
+ .isoformat()
130
+ .replace("+00:00", "Z"),
131
+ "pin_note": "Use this file with --pricing for reproducible historical reports.",
132
+ }
133
+ output_path.parent.mkdir(parents=True, exist_ok=True)
134
+ output_path.write_text(json.dumps(raw, indent=2, sort_keys=True) + "\n", encoding="utf-8")
135
+ return output_path
136
+
137
+
138
+ def parse_models(raw: object) -> dict[str, dict[str, float]]:
139
+ if not isinstance(raw, dict):
140
+ raise ValueError("pricing config must be a JSON object")
141
+ model_payload = raw.get("models", raw)
142
+ if not isinstance(model_payload, dict):
143
+ raise ValueError("pricing config 'models' must be an object")
144
+
145
+ models: dict[str, dict[str, float]] = {}
146
+ for model, rates in model_payload.items():
147
+ if not isinstance(model, str) or model.startswith("_"):
148
+ continue
149
+ if not isinstance(rates, dict):
150
+ continue
151
+ input_rate = _required_rate(rates, "input_per_million", model)
152
+ cached_rate = _optional_rate(rates, "cached_input_per_million")
153
+ output_rate = _required_rate(rates, "output_per_million", model)
154
+ models[model] = {
155
+ "input_per_million": float(input_rate),
156
+ "cached_input_per_million": float(
157
+ cached_rate if cached_rate is not None else input_rate
158
+ ),
159
+ "output_per_million": float(output_rate),
160
+ }
161
+ return models
162
+
163
+
164
+ def parse_aliases(raw: object) -> dict[str, str]:
165
+ if not isinstance(raw, dict):
166
+ return {}
167
+ aliases = raw.get("aliases")
168
+ if not isinstance(aliases, dict):
169
+ return {}
170
+ parsed: dict[str, str] = {}
171
+ for source, target in aliases.items():
172
+ if isinstance(source, str) and isinstance(target, str) and source and target:
173
+ parsed[source] = target
174
+ return parsed
175
+
176
+
177
+ def parse_estimated_models(raw: object) -> set[str]:
178
+ if not isinstance(raw, dict):
179
+ return set()
180
+ model_payload = raw.get("models", raw)
181
+ if not isinstance(model_payload, dict):
182
+ return set()
183
+ return {
184
+ model
185
+ for model, rates in model_payload.items()
186
+ if isinstance(model, str) and isinstance(rates, dict) and rates.get("estimated") is True
187
+ }
188
+
189
+
190
+ def load_existing_aliases(path: Path) -> dict[str, str]:
191
+ if not path.exists():
192
+ return {}
193
+ try:
194
+ return parse_aliases(json.loads(path.read_text(encoding="utf-8")))
195
+ except (OSError, TypeError, json.JSONDecodeError):
196
+ return {}
197
+
198
+
199
+ def _required_rate(rates: dict[str, Any], key: str, model: str) -> float:
200
+ value = _optional_rate(rates, key)
201
+ if value is None:
202
+ raise ValueError(f"missing {key} for model {model}")
203
+ return value
204
+
205
+
206
+ def _optional_rate(rates: dict[str, Any], key: str) -> float | None:
207
+ value = rates.get(key)
208
+ if value is None:
209
+ return None
210
+ parsed = _number(value)
211
+ if parsed < 0:
212
+ raise ValueError(f"{key} cannot be negative")
213
+ return parsed
214
+
215
+
216
+ def _number(value: object) -> float:
217
+ if isinstance(value, bool):
218
+ return float(int(value))
219
+ if isinstance(value, int | float):
220
+ return float(value)
221
+ if isinstance(value, str) and value.strip():
222
+ return float(value)
223
+ return 0.0
@@ -0,0 +1,44 @@
1
+ """Explicitly marked internal Codex model pricing estimates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ OPENAI_CODEX_LAUNCH_URL = "https://openai.com/index/introducing-codex/"
6
+ OPENAI_GPT_53_CODEX_MODEL_URL = "https://developers.openai.com/api/docs/models/gpt-5.3-codex"
7
+ OPENAI_CODEX_RATE_CARD_URL = "https://help.openai.com/en/articles/20001106-codex-rate-card"
8
+
9
+ PricingEstimateValue = float | bool | str
10
+
11
+ ESTIMATED_MODEL_PRICES: dict[str, dict[str, PricingEstimateValue]] = {
12
+ "codex-auto-review": {
13
+ "input_per_million": 1.5,
14
+ "cached_input_per_million": 0.375,
15
+ "output_per_million": 6.0,
16
+ "estimated": True,
17
+ "estimate_basis_model": "codex-mini-latest",
18
+ "estimate_source_url": OPENAI_CODEX_LAUNCH_URL,
19
+ "estimate_reason": (
20
+ "codex-auto-review is an internal Codex model label without a public "
21
+ "pricing row; estimate uses OpenAI-published codex-mini-latest rates."
22
+ ),
23
+ },
24
+ "gpt-5.3-codex-spark": {
25
+ "input_per_million": 1.75,
26
+ "cached_input_per_million": 0.175,
27
+ "output_per_million": 14.0,
28
+ "estimated": True,
29
+ "estimate_basis_model": "gpt-5.3-codex",
30
+ "estimate_source_url": OPENAI_GPT_53_CODEX_MODEL_URL,
31
+ "estimate_reference_url": OPENAI_CODEX_RATE_CARD_URL,
32
+ "estimate_reason": (
33
+ "GPT-5.3-Codex-Spark is listed by OpenAI as a research preview "
34
+ "without final Codex credit rates; estimate uses the published "
35
+ "GPT-5.3-Codex text-token rates until Spark rates are finalized."
36
+ ),
37
+ },
38
+ }
39
+
40
+
41
+ def estimated_model_prices() -> dict[str, dict[str, PricingEstimateValue]]:
42
+ """Return a copy of configured internal model estimates."""
43
+
44
+ return {model: dict(rates) for model, rates in ESTIMATED_MODEL_PRICES.items()}
@@ -0,0 +1,253 @@
1
+ """OpenAI pricing source fetching, parsing, and cache updates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import shutil
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any
13
+ from urllib.error import URLError
14
+ from urllib.request import Request, urlopen
15
+
16
+ from codex_usage_tracker import __version__
17
+ from codex_usage_tracker.paths import DEFAULT_PRICING_PATH
18
+ from codex_usage_tracker.pricing_config import PRICING_SCHEMA, load_existing_aliases
19
+ from codex_usage_tracker.pricing_estimates import ESTIMATED_MODEL_PRICES, estimated_model_prices
20
+
21
+ OPENAI_PRICING_MD_URL = "https://developers.openai.com/api/docs/pricing.md"
22
+ VALID_PRICING_TIERS = ("standard", "batch", "flex", "priority")
23
+
24
+
25
+ class PricingParseError(ValueError):
26
+ """Raised when the OpenAI pricing Markdown structure cannot be parsed."""
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class PricingUpdateResult:
31
+ """Result from refreshing the local pricing cache."""
32
+
33
+ path: Path
34
+ source_url: str
35
+ tier: str
36
+ fetched_at: str
37
+ model_count: int
38
+ estimated_model_count: int = 0
39
+ backup_path: Path | None = None
40
+
41
+
42
+ def update_pricing_from_openai_docs(
43
+ path: Path = DEFAULT_PRICING_PATH,
44
+ *,
45
+ tier: str = "standard",
46
+ source_url: str = OPENAI_PRICING_MD_URL,
47
+ fetch_text: Callable[[str], str] | None = None,
48
+ include_estimates: bool = True,
49
+ ) -> PricingUpdateResult:
50
+ """Fetch OpenAI-published pricing rows and cache them in the local config."""
51
+
52
+ if tier not in VALID_PRICING_TIERS:
53
+ raise ValueError(
54
+ f"unknown pricing tier {tier!r}; expected one of {', '.join(VALID_PRICING_TIERS)}"
55
+ )
56
+ fetcher = fetch_text or _fetch_text
57
+ text = fetcher(source_url)
58
+ parsed_models = parse_openai_pricing_markdown(text, tier=tier)
59
+ if not parsed_models:
60
+ raise PricingParseError(
61
+ f"pricing source schema changed: no text-token pricing rows were parsed "
62
+ f"from {source_url} for tier {tier!r}"
63
+ )
64
+ models: dict[str, dict[str, Any]] = {
65
+ model: dict(rates) for model, rates in parsed_models.items()
66
+ }
67
+ aliases = load_existing_aliases(path)
68
+ estimated_model_count = 0
69
+ if include_estimates:
70
+ models.update(estimated_model_prices())
71
+ estimated_model_count = len(ESTIMATED_MODEL_PRICES)
72
+
73
+ fetched_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
74
+ payload = {
75
+ "_schema": PRICING_SCHEMA,
76
+ "_source": {
77
+ "name": "OpenAI Developers pricing docs",
78
+ "url": source_url,
79
+ "tier": tier,
80
+ "fetched_at": fetched_at,
81
+ "model_count": len(models),
82
+ "official_model_count": len(models) - estimated_model_count,
83
+ "estimated_model_count": estimated_model_count,
84
+ },
85
+ "models": models,
86
+ }
87
+ if aliases:
88
+ payload["aliases"] = aliases
89
+ path.parent.mkdir(parents=True, exist_ok=True)
90
+ backup_path = _backup_existing_pricing(path)
91
+ tmp_path = path.with_name(f"{path.name}.tmp")
92
+ tmp_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
93
+ tmp_path.replace(path)
94
+ return PricingUpdateResult(
95
+ path=path,
96
+ source_url=source_url,
97
+ tier=tier,
98
+ fetched_at=fetched_at,
99
+ model_count=len(models),
100
+ estimated_model_count=estimated_model_count,
101
+ backup_path=backup_path,
102
+ )
103
+
104
+
105
+ def parse_openai_pricing_markdown(
106
+ markdown: str, *, tier: str = "standard"
107
+ ) -> dict[str, dict[str, float]]:
108
+ """Parse text-token rows from OpenAI's pricing markdown for one service tier."""
109
+
110
+ if tier not in VALID_PRICING_TIERS:
111
+ raise ValueError(
112
+ f"unknown pricing tier {tier!r}; expected one of {', '.join(VALID_PRICING_TIERS)}"
113
+ )
114
+ rows_block = _extract_text_token_rows_block(markdown, tier)
115
+ models: dict[str, dict[str, float]] = {}
116
+ for match in _OPENAI_PRICE_ROW_RE.finditer(rows_block):
117
+ model = _normalize_model_name(match.group("model"))
118
+ input_rate = _parse_openai_price_value(match.group("input"))
119
+ cached_rate = _parse_openai_price_value(match.group("cached"))
120
+ output_rate = _parse_openai_price_value(match.group("output"))
121
+ if not model or input_rate is None or output_rate is None:
122
+ continue
123
+ if cached_rate is None:
124
+ cached_rate = input_rate
125
+ models[model] = {
126
+ "input_per_million": input_rate,
127
+ "cached_input_per_million": cached_rate,
128
+ "output_per_million": output_rate,
129
+ }
130
+ if not models:
131
+ raise PricingParseError(
132
+ f"pricing source schema changed: tier {tier!r} rows block contained no "
133
+ "parseable text-token pricing rows"
134
+ )
135
+ return models
136
+
137
+
138
+ _OPENAI_PRICE_ROW_RE = re.compile(
139
+ r"""\[
140
+ \s*"(?P<model>[^"]+)"\s*,
141
+ \s*(?P<input>[^,\]\n]+)\s*,
142
+ \s*(?P<cached>[^,\]\n]+)\s*,
143
+ \s*(?P<output>[^,\]\n]+)\s*
144
+ \]""",
145
+ re.VERBOSE,
146
+ )
147
+
148
+
149
+ def _fetch_text(url: str) -> str:
150
+ request = Request(
151
+ url,
152
+ headers={
153
+ "Accept": "text/markdown,text/plain;q=0.9,*/*;q=0.1",
154
+ "User-Agent": f"codex-usage-tracker/{__version__}",
155
+ },
156
+ )
157
+ try:
158
+ with urlopen(request, timeout=20) as response:
159
+ return response.read().decode("utf-8")
160
+ except URLError as exc:
161
+ raise RuntimeError(f"could not fetch pricing source {url}: {exc}") from exc
162
+
163
+
164
+ def _backup_existing_pricing(path: Path) -> Path | None:
165
+ if not path.exists():
166
+ return None
167
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
168
+ backup_path = path.with_name(f"{path.name}.{stamp}.bak")
169
+ shutil.copy2(path, backup_path)
170
+ return backup_path
171
+
172
+
173
+ def _extract_text_token_rows_block(markdown: str, tier: str) -> str:
174
+ tier_marker = f'tier="{tier}"'
175
+ tier_index = markdown.find(tier_marker)
176
+ if tier_index == -1:
177
+ raise PricingParseError(
178
+ f"pricing source schema changed: could not find text-token tier marker {tier_marker!r}"
179
+ )
180
+ search_end = _pricing_component_end(markdown, tier_index)
181
+ rows_marker_index = markdown.find("rows={[", tier_index, search_end)
182
+ if rows_marker_index == -1:
183
+ raise PricingParseError(
184
+ f"pricing source schema changed: tier {tier!r} does not contain a rows={{[ block"
185
+ )
186
+ bracket_index = markdown.find("[", rows_marker_index, search_end)
187
+ if bracket_index == -1:
188
+ raise PricingParseError(
189
+ f"pricing source schema changed: tier {tier!r} has a malformed rows block"
190
+ )
191
+ end_index = _find_matching_bracket(markdown, bracket_index)
192
+ if end_index > search_end:
193
+ raise PricingParseError(
194
+ f"pricing source schema changed: tier {tier!r} rows block extends past its component"
195
+ )
196
+ return markdown[bracket_index + 1 : end_index]
197
+
198
+
199
+ def _pricing_component_end(markdown: str, tier_index: int) -> int:
200
+ candidates = [
201
+ index
202
+ for index in (
203
+ markdown.find("/>", tier_index),
204
+ markdown.find("</TextTokenPricingTables>", tier_index),
205
+ markdown.find("<TextTokenPricingTables", tier_index + 1),
206
+ )
207
+ if index != -1
208
+ ]
209
+ return min(candidates) if candidates else len(markdown)
210
+
211
+
212
+ def _find_matching_bracket(text: str, start_index: int) -> int:
213
+ depth = 0
214
+ quote: str | None = None
215
+ escaped = False
216
+ for index in range(start_index, len(text)):
217
+ char = text[index]
218
+ if quote:
219
+ if escaped:
220
+ escaped = False
221
+ elif char == "\\":
222
+ escaped = True
223
+ elif char == quote:
224
+ quote = None
225
+ continue
226
+ if char in {'"', "'", "`"}:
227
+ quote = char
228
+ elif char == "[":
229
+ depth += 1
230
+ elif char == "]":
231
+ depth -= 1
232
+ if depth == 0:
233
+ return index
234
+ raise PricingParseError("pricing source schema changed: rows block is unterminated")
235
+
236
+
237
+ def _normalize_model_name(model: str) -> str:
238
+ return re.sub(r"\s+\([^)]*context length[^)]*\)\s*$", "", model.strip(), flags=re.I)
239
+
240
+
241
+ def _parse_openai_price_value(value: str) -> float | None:
242
+ normalized = value.strip()
243
+ if normalized in {"", "null", "undefined", "-", '""', "''", '"-"', "'-'"}:
244
+ return None
245
+ if (
246
+ len(normalized) >= 2
247
+ and normalized[0] == normalized[-1]
248
+ and normalized[0] in {'"', "'"}
249
+ ):
250
+ normalized = normalized[1:-1].strip()
251
+ if normalized in {"", "-", "Free"}:
252
+ return None
253
+ return float(normalized.replace("_", ""))