tidegap 0.18.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.
tidegap/__init__.py ADDED
@@ -0,0 +1,226 @@
1
+ """Tidegap — carbon-aware scheduling for agentic AI workflows.
2
+
3
+ Python port of ``@tidegap/core``. Public surface mirrors the
4
+ TypeScript package so the two stay in sync.
5
+
6
+ Quick start
7
+ -----------
8
+
9
+ >>> import asyncio
10
+ >>> from tidegap import defer
11
+ >>> async def main():
12
+ ... return await defer(
13
+ ... lambda: 42,
14
+ ... deadline="2099-01-01T00:00:00Z",
15
+ ... carbon_budget_g=5,
16
+ ... region="US-CAL-CISO",
17
+ ... )
18
+ >>> asyncio.run(main())
19
+ 42
20
+
21
+ The default scheduler runs in-memory. For durable receipts and
22
+ crash-recovery, instantiate a :class:`Scheduler` with a SQLite
23
+ ``db_path``:
24
+
25
+ >>> from tidegap import Scheduler
26
+ >>> scheduler = Scheduler(db_path="/var/lib/tidegap/queue.sqlite")
27
+
28
+ See ``packages/core-py/README.md`` for the full API reference.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from importlib.metadata import PackageNotFoundError
34
+ from importlib.metadata import version as _pkg_version
35
+
36
+ from .budget import (
37
+ CarbonAlert,
38
+ CarbonBudgetConfig,
39
+ CarbonBudgetStatus,
40
+ CarbonBudgetUsage,
41
+ CarbonBudgetWindowKind,
42
+ carbon_budget_config_path,
43
+ carbon_budget_status,
44
+ carbon_budget_usage,
45
+ load_carbon_budget_config,
46
+ receipt_carbon_g,
47
+ window_bounds,
48
+ )
49
+ from .energy import (
50
+ DEFAULT_PUE,
51
+ ENERGY_SOURCES,
52
+ LEGACY_KWH_PER_TASK,
53
+ MODEL_ENERGY_COEFFICIENTS,
54
+ MODEL_FAMILIES,
55
+ EnergyResolutionTier,
56
+ EnergySourceTier,
57
+ ModelEnergyCoefficients,
58
+ ResolvedModelEnergy,
59
+ estimate_energy_kwh,
60
+ grams_for_intensity,
61
+ lookup_model_energy,
62
+ normalize_model_name,
63
+ resolve_model_energy,
64
+ )
65
+ from .errors import (
66
+ CarbonBudgetExceededError,
67
+ InvalidDeadlineError,
68
+ SchedulerShutdownError,
69
+ TaskCancelledError,
70
+ )
71
+ from .grid import (
72
+ EntsoePeriod,
73
+ EntsoeTimeSeries,
74
+ GridFeed,
75
+ build_default_grid_feed,
76
+ eia_feed,
77
+ electricity_maps_feed,
78
+ entsoe_feed,
79
+ mock_grid_feed,
80
+ multi_source_grid_feed,
81
+ parse_entsoe_xml,
82
+ uk_carbon_intensity_feed,
83
+ watttime_feed,
84
+ )
85
+ from .recommend import (
86
+ RecommendAlternative,
87
+ RecommendResult,
88
+ recommend_window,
89
+ )
90
+ from .scheduler import (
91
+ DEFAULT_REGION,
92
+ ENERGY_KWH_PER_TASK,
93
+ MAX_HORIZON_HOURS,
94
+ TOLERANCE_FLOOR_G,
95
+ TOLERANCE_FRACTION,
96
+ DeferrableTask,
97
+ Scheduler,
98
+ SelectWindowResult,
99
+ defer,
100
+ normalize_deadline,
101
+ pick_best_window,
102
+ select_window,
103
+ )
104
+ from .sign import (
105
+ SigningKeyPair,
106
+ SigningNotInstalled,
107
+ VerifyOutcome,
108
+ VerifyResult,
109
+ default_signing_key_path,
110
+ is_signing_available,
111
+ load_or_create_signing_key,
112
+ sign_receipt,
113
+ verify_receipt,
114
+ )
115
+ from .types import (
116
+ Band,
117
+ CarbonReceipt,
118
+ DeferOptions,
119
+ EnergyResolution,
120
+ EnergySource,
121
+ ForecastKind,
122
+ GridForecast,
123
+ GridForecastEntry,
124
+ GridSignalType,
125
+ GridSource,
126
+ IntensitySource,
127
+ Provider,
128
+ ProviderCallSpec,
129
+ TaskRecord,
130
+ TaskStatus,
131
+ TickResult,
132
+ TickResultEntry,
133
+ )
134
+
135
+ # The package version is declared once, in pyproject.toml. Read it back from
136
+ # the installed metadata rather than hard-coding a second copy here.
137
+ try:
138
+ __version__ = _pkg_version("tidegap")
139
+ except PackageNotFoundError: # pragma: no cover - source tree without install
140
+ __version__ = "0+unknown"
141
+
142
+ __all__ = [
143
+ "DEFAULT_PUE",
144
+ "DEFAULT_REGION",
145
+ "ENERGY_KWH_PER_TASK",
146
+ "ENERGY_SOURCES",
147
+ "LEGACY_KWH_PER_TASK",
148
+ "MAX_HORIZON_HOURS",
149
+ "MODEL_ENERGY_COEFFICIENTS",
150
+ "MODEL_FAMILIES",
151
+ "TOLERANCE_FLOOR_G",
152
+ "TOLERANCE_FRACTION",
153
+ "Band",
154
+ "CarbonAlert",
155
+ "CarbonBudgetConfig",
156
+ "CarbonBudgetExceededError",
157
+ "CarbonBudgetStatus",
158
+ "CarbonBudgetUsage",
159
+ "CarbonBudgetWindowKind",
160
+ "CarbonReceipt",
161
+ "DeferOptions",
162
+ "DeferrableTask",
163
+ "EnergyResolution",
164
+ "EnergyResolutionTier",
165
+ "EnergySource",
166
+ "EnergySourceTier",
167
+ "EntsoePeriod",
168
+ "EntsoeTimeSeries",
169
+ "ForecastKind",
170
+ "GridFeed",
171
+ "GridForecast",
172
+ "GridForecastEntry",
173
+ "GridSignalType",
174
+ "GridSource",
175
+ "IntensitySource",
176
+ "InvalidDeadlineError",
177
+ "ModelEnergyCoefficients",
178
+ "Provider",
179
+ "ProviderCallSpec",
180
+ "RecommendAlternative",
181
+ "RecommendResult",
182
+ "ResolvedModelEnergy",
183
+ "Scheduler",
184
+ "SchedulerShutdownError",
185
+ "SelectWindowResult",
186
+ "SigningKeyPair",
187
+ "SigningNotInstalled",
188
+ "TaskCancelledError",
189
+ "TaskRecord",
190
+ "TaskStatus",
191
+ "TickResult",
192
+ "TickResultEntry",
193
+ "VerifyOutcome",
194
+ "VerifyResult",
195
+ "__version__",
196
+ "build_default_grid_feed",
197
+ "carbon_budget_config_path",
198
+ "carbon_budget_status",
199
+ "carbon_budget_usage",
200
+ "default_signing_key_path",
201
+ "defer",
202
+ "eia_feed",
203
+ "electricity_maps_feed",
204
+ "entsoe_feed",
205
+ "estimate_energy_kwh",
206
+ "grams_for_intensity",
207
+ "is_signing_available",
208
+ "load_carbon_budget_config",
209
+ "load_or_create_signing_key",
210
+ "lookup_model_energy",
211
+ "mock_grid_feed",
212
+ "multi_source_grid_feed",
213
+ "normalize_deadline",
214
+ "normalize_model_name",
215
+ "parse_entsoe_xml",
216
+ "pick_best_window",
217
+ "receipt_carbon_g",
218
+ "recommend_window",
219
+ "resolve_model_energy",
220
+ "select_window",
221
+ "sign_receipt",
222
+ "uk_carbon_intensity_feed",
223
+ "verify_receipt",
224
+ "watttime_feed",
225
+ "window_bounds",
226
+ ]
tidegap/_data.py ADDED
@@ -0,0 +1,245 @@
1
+ # GENERATED — DO NOT EDIT.
2
+ #
3
+ # Produced by scripts/gen-data.mjs from packages/core-ts/src/data/*.json.
4
+ # Edit those JSON files and run 'pnpm gen:data' to regenerate. CI fails
5
+ # on drift ('pnpm gen:data:check').
6
+ # ruff: noqa
7
+ """Generated data tables mirrored from the JSON SSOT in @tidegap/core."""
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ #: Industry-average Power Usage Effectiveness for hyperscaler data centres.
14
+ DEFAULT_PUE: float = 1.15
15
+
16
+ #: Backwards-compatible flat estimate. Used when no model is provided.
17
+ LEGACY_KWH_PER_TASK: float = 0.0015
18
+
19
+ #: Typical token shape used when a caller names a model but no token counts.
20
+ TYPICAL_INPUT_TOKENS: int = 500
21
+ TYPICAL_OUTPUT_TOKENS: int = 500
22
+
23
+ #: Per-model coefficient table. Keys are canonical lowercase names.
24
+ COEFFICIENTS: dict[str, dict[str, Any]] = {
25
+ "claude-fable-5-1": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
26
+ "claude-fable-5": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
27
+ "claude-opus-5-5": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
28
+ "claude-opus-5": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
29
+ "claude-opus-4-8": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
30
+ "claude-opus-4": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": "deprecated"},
31
+ "claude-opus-4-7": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": None},
32
+ "claude-opus-4-6": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": None},
33
+ "claude-opus-4-5": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
34
+ "claude-opus-4-1": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": "retired"},
35
+ "claude-opus-3": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": "retired"},
36
+ "claude-sonnet-5": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": None, "source": "estimated", "status": None},
37
+ "claude-sonnet-4": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "estimated", "status": "deprecated"},
38
+ "claude-sonnet-4-6": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "estimated", "status": None},
39
+ "claude-sonnet-4-5": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "estimated", "status": None},
40
+ "claude-sonnet-3-7": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "estimated", "status": "retired"},
41
+ "claude-sonnet-3-5": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "estimated", "status": "retired"},
42
+ "claude-sonnet-3": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "estimated", "status": "retired"},
43
+ "claude-haiku-4-5": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": 13, "source": "estimated", "status": None},
44
+ "claude-haiku-3-5": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": 13, "source": "estimated", "status": "retired"},
45
+ "claude-haiku-3": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": 13, "source": "estimated", "status": "retired"},
46
+ "gpt-6-astra": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
47
+ "gpt-6-sol": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": None, "source": "estimated", "status": None},
48
+ "gpt-6-luna": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": None, "source": "estimated", "status": None},
49
+ "gpt-5": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
50
+ "gpt-5-mini": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": None, "source": "estimated", "status": None},
51
+ "gpt-5-nano": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": None, "source": "estimated", "status": None},
52
+ "gpt-4o": {"wh_per_input_token": 0.002, "wh_per_output_token": 0.01, "params_b": 200, "source": "estimated", "status": None},
53
+ "gpt-4o-mini": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": 30, "source": "estimated", "status": None},
54
+ "gpt-4-1": {"wh_per_input_token": 0.002, "wh_per_output_token": 0.01, "params_b": None, "source": "estimated", "status": None},
55
+ "gpt-4-1-mini": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": None, "source": "estimated", "status": None},
56
+ "gpt-4-1-nano": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": None, "source": "estimated", "status": None},
57
+ "gpt-4-turbo": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": "deprecated"},
58
+ "gpt-4": {"wh_per_input_token": 0.005, "wh_per_output_token": 0.025, "params_b": 1000, "source": "estimated", "status": "deprecated"},
59
+ "gpt-3-5-turbo": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": 20, "source": "estimated", "status": None},
60
+ "o1": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": None},
61
+ "o1-mini": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": 30, "source": "estimated", "status": "retired"},
62
+ "o3": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": 400, "source": "estimated", "status": None},
63
+ "o3-mini": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": 30, "source": "estimated", "status": "deprecated"},
64
+ "gemini-3-1-pro": {"wh_per_input_token": 0.002, "wh_per_output_token": 0.01, "params_b": None, "source": "estimated", "status": None},
65
+ "gemini-3-8-flash": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": None, "source": "estimated", "status": None},
66
+ "gemini-3-5-flash-lite": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": None, "source": "estimated", "status": None},
67
+ "gemini-1-5-pro": {"wh_per_input_token": 0.002, "wh_per_output_token": 0.01, "params_b": 200, "source": "estimated", "status": "retired"},
68
+ "gemini-1-5-flash": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": 20, "source": "estimated", "status": "retired"},
69
+ "gemini-2-0-flash": {"wh_per_input_token": 0.0003, "wh_per_output_token": 0.0015, "params_b": 20, "source": "estimated", "status": "retired"},
70
+ "gemini-2-0-pro": {"wh_per_input_token": 0.002, "wh_per_output_token": 0.01, "params_b": 200, "source": "estimated", "status": "retired"},
71
+ "llama-3-1-405b": {"wh_per_input_token": 0.005, "wh_per_output_token": 0.025, "params_b": 405, "source": "measured", "status": None},
72
+ "llama-3-1-70b": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "measured", "status": None},
73
+ "llama-3-1-8b": {"wh_per_input_token": 0.0002, "wh_per_output_token": 0.001, "params_b": 8, "source": "measured", "status": None},
74
+ "llama-3-70b": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": 70, "source": "measured", "status": None},
75
+ "llama-3-8b": {"wh_per_input_token": 0.0002, "wh_per_output_token": 0.001, "params_b": 8, "source": "measured", "status": None},
76
+ "llama-4-scout": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": None, "source": "estimated", "status": None},
77
+ "llama-4-maverick": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": None, "source": "estimated", "status": None},
78
+ "mistral-small-4": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": None, "source": "estimated", "status": None},
79
+ "mistral-medium-3-5": {"wh_per_input_token": 0.001, "wh_per_output_token": 0.005, "params_b": None, "source": "estimated", "status": None},
80
+ "mistral-large-3": {"wh_per_input_token": 0.003, "wh_per_output_token": 0.015, "params_b": None, "source": "estimated", "status": None},
81
+ "mistral-7b": {"wh_per_input_token": 0.0002, "wh_per_output_token": 0.001, "params_b": 7, "source": "measured", "status": None},
82
+ "mixtral-8x7b": {"wh_per_input_token": 0.0006, "wh_per_output_token": 0.003, "params_b": 47, "source": "measured", "status": None},
83
+ "mixtral-8x22b": {"wh_per_input_token": 0.0015, "wh_per_output_token": 0.0075, "params_b": 141, "source": "measured", "status": None},
84
+ }
85
+
86
+ #: Ordered family-fallback rules (see _family_representative).
87
+ FAMILIES: list[dict[str, Any]] = [
88
+ {"id": "claude-fable", "representative": "claude-fable-5-1", "contains": ["fable"], "prefix": None, "regex": None},
89
+ {"id": "claude-mythos", "representative": "claude-fable-5-1", "contains": ["mythos"], "prefix": None, "regex": None},
90
+ {"id": "claude-opus", "representative": "claude-opus-5", "contains": ["opus"], "prefix": None, "regex": None},
91
+ {"id": "claude-sonnet", "representative": "claude-sonnet-5", "contains": ["sonnet"], "prefix": None, "regex": None},
92
+ {"id": "claude-haiku", "representative": "claude-haiku-4-5", "contains": ["haiku"], "prefix": None, "regex": None},
93
+ {"id": "gpt-6", "representative": "gpt-6-sol", "contains": None, "prefix": "gpt-6", "regex": None},
94
+ {"id": "gpt-5", "representative": "gpt-5", "contains": None, "prefix": "gpt-5", "regex": None},
95
+ {"id": "gpt-4o", "representative": "gpt-4o", "contains": None, "prefix": "gpt-4o", "regex": None},
96
+ {"id": "gpt-4-1", "representative": "gpt-4-1", "contains": None, "prefix": "gpt-4-1", "regex": None},
97
+ {"id": "gpt-4", "representative": "gpt-4", "contains": None, "prefix": "gpt-4", "regex": None},
98
+ {"id": "gpt-3", "representative": "gpt-3-5-turbo", "contains": None, "prefix": "gpt-3", "regex": None},
99
+ {"id": "openai-o", "representative": "o3", "contains": None, "prefix": None, "regex": "^o[0-9]"},
100
+ {"id": "gemini-flash", "representative": "gemini-3-8-flash", "contains": ["flash"], "prefix": "gemini", "regex": None},
101
+ {"id": "gemini-pro", "representative": "gemini-3-1-pro", "contains": None, "prefix": "gemini", "regex": None},
102
+ {"id": "mixtral", "representative": "mixtral-8x7b", "contains": None, "prefix": "mixtral", "regex": None},
103
+ {"id": "mistral", "representative": "mistral-7b", "contains": None, "prefix": "mistral", "regex": None},
104
+ {"id": "llama", "representative": "llama-3-1-70b", "contains": None, "prefix": "llama", "regex": None},
105
+ ]
106
+
107
+ #: Citation metadata for the coefficient table.
108
+ ENERGY_SOURCES: dict[str, dict[str, str]] = {
109
+ "patterson2021": {"citation": "Patterson et al. 2021, 'Carbon Emissions and Large Neural Network Training'", "arxiv": "2104.10350"},
110
+ "luccioni2024": {"citation": "Luccioni, Jernite, Strubell 2024, 'Power Hungry Processing'", "venue": "FAccT 2024", "arxiv": "2311.16863"},
111
+ "huggingface": {"citation": "Hugging Face AI Energy Score", "url": "https://huggingface.co/AIEnergyScore"},
112
+ }
113
+
114
+ #: Synthetic-curve region midpoints (gCO2/kWh).
115
+ REGION_FLOORS: dict[str, int] = {
116
+ "US-CAL-CISO": 280,
117
+ "US-TEX-ERCO": 340,
118
+ "US-NE-ISNE": 320,
119
+ "US-MIDA-PJM": 420,
120
+ "GB": 220,
121
+ "FR": 60,
122
+ "DE": 380,
123
+ "IE": 340,
124
+ "NL": 350,
125
+ "ES": 160,
126
+ "BE": 140,
127
+ "AT": 110,
128
+ "PL": 650,
129
+ "JP-TK": 470,
130
+ "KR": 430,
131
+ "SG": 420,
132
+ "AU-NSW": 500,
133
+ "CA-ON": 50,
134
+ "US-NY-NYIS": 360,
135
+ "US-MIDW-MISO": 460,
136
+ "US-NW-BPAT": 110,
137
+ "US-FLA-FPL": 400,
138
+ "IT-NO": 320,
139
+ "SE-SE3": 40,
140
+ "NO-NO1": 30,
141
+ "FI": 120,
142
+ "DK-DK1": 180,
143
+ "IN-WE": 700,
144
+ "ZA": 700,
145
+ "AE": 620,
146
+ "NZ": 80,
147
+ }
148
+
149
+ #: Fallback midpoint for regions with no explicit floor.
150
+ DEFAULT_REGION_FLOOR: int = 380
151
+
152
+ #: Peak-to-trough half-swing of the synthetic curve (gCO2/kWh).
153
+ SYNTHETIC_AMPLITUDE: int = 220
154
+
155
+ #: Per-region UTC-hour offset applied to the synthetic curve's local trough.
156
+ REGION_UTC_OFFSETS: dict[str, int] = {
157
+ "US-CAL-CISO": -8,
158
+ "US-TEX-ERCO": -6,
159
+ "US-NE-ISNE": -5,
160
+ "US-MIDA-PJM": -5,
161
+ "GB": 0,
162
+ "FR": 1,
163
+ "DE": 1,
164
+ "IE": 0,
165
+ "NL": 1,
166
+ "ES": 1,
167
+ "BE": 1,
168
+ "AT": 1,
169
+ "PL": 1,
170
+ "JP-TK": 9,
171
+ "KR": 9,
172
+ "SG": 8,
173
+ "AU-NSW": 10,
174
+ "CA-ON": -5,
175
+ "US-NY-NYIS": -5,
176
+ "US-MIDW-MISO": -6,
177
+ "US-NW-BPAT": -8,
178
+ "US-FLA-FPL": -5,
179
+ "IT-NO": 1,
180
+ "SE-SE3": 1,
181
+ "NO-NO1": 1,
182
+ "FI": 2,
183
+ "DK-DK1": 1,
184
+ "IN-WE": 5,
185
+ "ZA": 2,
186
+ "AE": 4,
187
+ "NZ": 12,
188
+ }
189
+
190
+ #: Band thresholds (ascending). A value maps to the first band it is below.
191
+ BAND_THRESHOLDS: list[tuple[int, str]] = [
192
+ (100, "very_clean"),
193
+ (250, "clean"),
194
+ (450, "average"),
195
+ (700, "dirty"),
196
+ ]
197
+
198
+ #: Band for values at or above every threshold.
199
+ DEFAULT_BAND: str = "very_dirty"
200
+
201
+ #: Month the price table figures were read (see prices.json).
202
+ PRICES_AS_OF: str = "2026-09"
203
+
204
+ #: Per-model public list prices (USD per million tokens), keyed by the same
205
+ #: canonical lowercase ids as COEFFICIENTS. Ollama-routable open-weight models
206
+ #: are 0 (self-hosted). Used by cross-provider routing's cost dimension.
207
+ MODEL_PRICES: dict[str, dict[str, Any]] = {
208
+ "claude-fable-5-1": {"in_usd_per_mtok": 10, "out_usd_per_mtok": 50, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
209
+ "claude-fable-5": {"in_usd_per_mtok": 10, "out_usd_per_mtok": 50, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
210
+ "claude-opus-5-5": {"in_usd_per_mtok": 4, "out_usd_per_mtok": 20, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
211
+ "claude-opus-5": {"in_usd_per_mtok": 5, "out_usd_per_mtok": 25, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
212
+ "claude-opus-4-8": {"in_usd_per_mtok": 5, "out_usd_per_mtok": 25, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
213
+ "claude-opus-4": {"in_usd_per_mtok": 15, "out_usd_per_mtok": 75, "batch_discount": 0.5, "as_of": "2026-07", "source": "Anthropic pricing (anthropic.com/pricing)"},
214
+ "claude-opus-4-7": {"in_usd_per_mtok": 5, "out_usd_per_mtok": 25, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
215
+ "claude-opus-4-6": {"in_usd_per_mtok": 5, "out_usd_per_mtok": 25, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
216
+ "claude-sonnet-5": {"in_usd_per_mtok": 2, "out_usd_per_mtok": 10, "batch_discount": 0.5, "as_of": "2026-09", "source": "Anthropic pricing (platform.claude.com/docs/en/about-claude/pricing)"},
217
+ "claude-sonnet-4": {"in_usd_per_mtok": 3, "out_usd_per_mtok": 15, "batch_discount": 0.5, "as_of": "2026-07", "source": "Anthropic pricing (anthropic.com/pricing)"},
218
+ "claude-sonnet-4-6": {"in_usd_per_mtok": 3, "out_usd_per_mtok": 15, "batch_discount": 0.5, "as_of": "2026-07", "source": "Anthropic pricing (anthropic.com/pricing)"},
219
+ "claude-sonnet-4-5": {"in_usd_per_mtok": 3, "out_usd_per_mtok": 15, "batch_discount": 0.5, "as_of": "2026-07", "source": "Anthropic pricing (anthropic.com/pricing)"},
220
+ "claude-haiku-4-5": {"in_usd_per_mtok": 1, "out_usd_per_mtok": 5, "batch_discount": 0.5, "as_of": "2026-07", "source": "Anthropic pricing (anthropic.com/pricing)"},
221
+ "gpt-6-astra": {"in_usd_per_mtok": 10, "out_usd_per_mtok": 50, "batch_discount": 0.5, "as_of": "2026-09", "source": "OpenAI API pricing (developers.openai.com/api/docs/pricing)"},
222
+ "gpt-6-sol": {"in_usd_per_mtok": 2, "out_usd_per_mtok": 10, "batch_discount": 0.5, "as_of": "2026-09", "source": "OpenAI API pricing (developers.openai.com/api/docs/pricing)"},
223
+ "gpt-6-luna": {"in_usd_per_mtok": 0.1, "out_usd_per_mtok": 0.5, "batch_discount": 0.5, "as_of": "2026-09", "source": "OpenAI API pricing (developers.openai.com/api/docs/pricing)"},
224
+ "gpt-4o": {"in_usd_per_mtok": 2.5, "out_usd_per_mtok": 10, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
225
+ "gpt-4o-mini": {"in_usd_per_mtok": 0.15, "out_usd_per_mtok": 0.6, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
226
+ "gpt-4-turbo": {"in_usd_per_mtok": 10, "out_usd_per_mtok": 30, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
227
+ "gpt-4": {"in_usd_per_mtok": 30, "out_usd_per_mtok": 60, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
228
+ "gpt-3-5-turbo": {"in_usd_per_mtok": 0.5, "out_usd_per_mtok": 1.5, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
229
+ "o1": {"in_usd_per_mtok": 15, "out_usd_per_mtok": 60, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
230
+ "o3": {"in_usd_per_mtok": 2, "out_usd_per_mtok": 8, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
231
+ "o3-mini": {"in_usd_per_mtok": 1.1, "out_usd_per_mtok": 4.4, "batch_discount": 0.5, "as_of": "2026-07", "source": "OpenAI API pricing (openai.com/api/pricing)"},
232
+ "llama-3-1-405b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
233
+ "llama-3-1-70b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
234
+ "llama-3-1-8b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
235
+ "llama-3-70b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
236
+ "llama-3-8b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
237
+ "llama-4-scout": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-09", "source": "Ollama local (self-hosted; no per-token vendor price)"},
238
+ "llama-4-maverick": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-09", "source": "Ollama local (self-hosted; no per-token vendor price)"},
239
+ "mistral-small-4": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-09", "source": "Ollama local (self-hosted; no per-token vendor price)"},
240
+ "mistral-medium-3-5": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-09", "source": "Ollama local (self-hosted; no per-token vendor price)"},
241
+ "mistral-large-3": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-09", "source": "Ollama local (self-hosted; no per-token vendor price)"},
242
+ "mistral-7b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
243
+ "mixtral-8x7b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
244
+ "mixtral-8x22b": {"in_usd_per_mtok": 0, "out_usd_per_mtok": 0, "batch_discount": None, "as_of": "2026-07", "source": "Ollama local (self-hosted; no per-token vendor price)"},
245
+ }
tidegap/_env.py ADDED
@@ -0,0 +1,43 @@
1
+ """Read a Tidegap environment variable, falling back to its 0.17 ``EBB_*`` name.
2
+
3
+ ``TIDEGAP_<NAME>`` wins when set. Otherwise ``EBB_<NAME>`` is read, and the
4
+ first fallback for each name prints one deprecation line to stderr. The
5
+ fallback is removed in 0.19.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+
13
+ KEY_PREFIX = "TIDEGAP_"
14
+ LEGACY_KEY_PREFIX = "EBB_"
15
+
16
+ _reported: set[str] = set()
17
+
18
+
19
+ def env_value(name: str) -> str | None:
20
+ """``TIDEGAP_<name>``, else ``EBB_<name>`` (with one stderr line), else ``None``."""
21
+ current = os.environ.get(KEY_PREFIX + name)
22
+ if current is not None and current.strip() != "":
23
+ return current
24
+ legacy = os.environ.get(LEGACY_KEY_PREFIX + name)
25
+ if legacy is not None and legacy.strip() != "":
26
+ if name not in _reported:
27
+ _reported.add(name)
28
+ print(
29
+ f"tidegap: rename EBB_{name} -> TIDEGAP_{name}; "
30
+ "the EBB_* names stop working in 0.19.",
31
+ file=sys.stderr,
32
+ )
33
+ return legacy
34
+ return current
35
+
36
+
37
+ def with_tidegap_keys(values: dict[str, str]) -> dict[str, str]:
38
+ """Fold ``EBB_*`` keys into ``TIDEGAP_*`` names; a ``TIDEGAP_*`` key wins."""
39
+ out = {k: v for k, v in values.items() if not k.startswith(LEGACY_KEY_PREFIX)}
40
+ for key, value in values.items():
41
+ if key.startswith(LEGACY_KEY_PREFIX):
42
+ out.setdefault(KEY_PREFIX + key[len(LEGACY_KEY_PREFIX) :], value)
43
+ return out