ebb-ai 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ebb_ai-0.6.0/.gitignore +57 -0
- ebb_ai-0.6.0/PKG-INFO +399 -0
- ebb_ai-0.6.0/README.md +362 -0
- ebb_ai-0.6.0/pyproject.toml +83 -0
- ebb_ai-0.6.0/src/ebb_ai/__init__.py +121 -0
- ebb_ai-0.6.0/src/ebb_ai/energy.py +236 -0
- ebb_ai-0.6.0/src/ebb_ai/errors.py +61 -0
- ebb_ai-0.6.0/src/ebb_ai/grid.py +267 -0
- ebb_ai-0.6.0/src/ebb_ai/providers/__init__.py +31 -0
- ebb_ai-0.6.0/src/ebb_ai/providers/anthropic.py +155 -0
- ebb_ai-0.6.0/src/ebb_ai/providers/base.py +118 -0
- ebb_ai-0.6.0/src/ebb_ai/providers/openai.py +172 -0
- ebb_ai-0.6.0/src/ebb_ai/recommend.py +339 -0
- ebb_ai-0.6.0/src/ebb_ai/scheduler.py +1428 -0
- ebb_ai-0.6.0/src/ebb_ai/types.py +289 -0
- ebb_ai-0.6.0/tests/__init__.py +0 -0
- ebb_ai-0.6.0/tests/test_energy.py +151 -0
- ebb_ai-0.6.0/tests/test_grid.py +151 -0
- ebb_ai-0.6.0/tests/test_providers.py +285 -0
- ebb_ai-0.6.0/tests/test_recommend.py +307 -0
- ebb_ai-0.6.0/tests/test_scheduler.py +430 -0
- ebb_ai-0.6.0/tests/test_tick.py +541 -0
ebb_ai-0.6.0/.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
|
|
5
|
+
# Build output
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.tsbuildinfo
|
|
9
|
+
.next/
|
|
10
|
+
out/
|
|
11
|
+
|
|
12
|
+
# Python
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
*$py.class
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
|
|
22
|
+
# Environment
|
|
23
|
+
.env
|
|
24
|
+
.env.local
|
|
25
|
+
.env.*.local
|
|
26
|
+
!.env.example
|
|
27
|
+
|
|
28
|
+
# Logs
|
|
29
|
+
*.log
|
|
30
|
+
npm-debug.log*
|
|
31
|
+
pnpm-debug.log*
|
|
32
|
+
|
|
33
|
+
# IDE
|
|
34
|
+
.vscode/
|
|
35
|
+
.idea/
|
|
36
|
+
*.swp
|
|
37
|
+
*.swo
|
|
38
|
+
|
|
39
|
+
# OS
|
|
40
|
+
.DS_Store
|
|
41
|
+
Thumbs.db
|
|
42
|
+
|
|
43
|
+
# Local data (SQLite scheduler queue)
|
|
44
|
+
*.db
|
|
45
|
+
*.db-journal
|
|
46
|
+
*.sqlite
|
|
47
|
+
*.sqlite3
|
|
48
|
+
ebb-queue.db
|
|
49
|
+
|
|
50
|
+
# Coverage
|
|
51
|
+
coverage/
|
|
52
|
+
*.lcov
|
|
53
|
+
|
|
54
|
+
# Temporary
|
|
55
|
+
tmp/
|
|
56
|
+
.cache/
|
|
57
|
+
development/
|
ebb_ai-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ebb-ai
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Carbon-aware scheduling for agentic AI workflows.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Vitalini/ebb-ai
|
|
6
|
+
Project-URL: Issues, https://github.com/Vitalini/ebb-ai/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/Vitalini/ebb-ai
|
|
8
|
+
Author: Vitalii Borovyk
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: agents,ai,carbon-aware,llm,mcp,scheduling,sustainability
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: aiosqlite>=0.20
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: anthropic
|
|
27
|
+
Requires-Dist: anthropic>=0.39; extra == 'anthropic'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: anthropic>=0.39; extra == 'dev'
|
|
30
|
+
Requires-Dist: openai>=1.50; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
34
|
+
Provides-Extra: openai
|
|
35
|
+
Requires-Dist: openai>=1.50; extra == 'openai'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# ebb-ai (Python)
|
|
39
|
+
|
|
40
|
+
**Carbon-aware scheduling for agentic AI workflows.**
|
|
41
|
+
|
|
42
|
+
`ebb-ai` defers non-urgent LLM calls to execution windows that are
|
|
43
|
+
simultaneously cleaner on the electricity grid, cheaper at the
|
|
44
|
+
provider, and friendlier to your hardware budget. The same agent code
|
|
45
|
+
that would have made a synchronous LLM call now hands the work to
|
|
46
|
+
`ebb-ai`, which picks the right time and the right route — and writes
|
|
47
|
+
a per-task carbon receipt you can audit.
|
|
48
|
+
|
|
49
|
+
This package is the Python port of [`@ebb-ai/core`](../core-ts/). The
|
|
50
|
+
two stay in lock-step on every public name; the only deliberate
|
|
51
|
+
asymmetry is that the Python port ships **SQLite-backed durable
|
|
52
|
+
persistence** from day one, while the TypeScript port is still
|
|
53
|
+
in-memory at v0.1. See [`ROADMAP.md`](../../ROADMAP.md) section 4.1.
|
|
54
|
+
|
|
55
|
+
> Status: v0.2 · 2026-05 · pre-PyPI, install from source.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Why this exists
|
|
60
|
+
|
|
61
|
+
Modern AI agents call LLM APIs synchronously by default. Three costs follow:
|
|
62
|
+
|
|
63
|
+
- **Carbon.** Grid carbon intensity varies 30–60% inside a single day
|
|
64
|
+
across the major US ISOs. Inference at 2 p.m. on a hot day is
|
|
65
|
+
materially dirtier than the same call at 3 a.m.
|
|
66
|
+
- **Dollars.** Anthropic and OpenAI both offer batch APIs at a flat
|
|
67
|
+
50% discount for tasks that can wait up to 24 hours. Almost no agent
|
|
68
|
+
code uses them by default because it requires rewriting the call
|
|
69
|
+
site.
|
|
70
|
+
- **Latency, honestly.** Off-peak *sync* execution is sometimes
|
|
71
|
+
faster because providers throttle and queue at peak. **Batch API
|
|
72
|
+
is *not* faster** — it trades latency (up to 24h SLA) for the
|
|
73
|
+
50% discount.
|
|
74
|
+
|
|
75
|
+
`ebb-ai` fixes all three for any task that is not "answer me right now."
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# from source (until the first PyPI release)
|
|
83
|
+
pip install -e packages/core-py
|
|
84
|
+
|
|
85
|
+
# with vendor extras
|
|
86
|
+
pip install -e "packages/core-py[anthropic,openai]"
|
|
87
|
+
|
|
88
|
+
# dev install for contributors
|
|
89
|
+
pip install -e "packages/core-py[dev,anthropic,openai]"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Python 3.11+. The core library only requires `httpx` and `aiosqlite`;
|
|
93
|
+
the Anthropic and OpenAI extras pull in the official vendor SDKs.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Quick start
|
|
98
|
+
|
|
99
|
+
### 1. The five-line version
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import asyncio
|
|
103
|
+
from ebb_ai import defer
|
|
104
|
+
|
|
105
|
+
async def main():
|
|
106
|
+
result = await defer(
|
|
107
|
+
lambda: "do the work here",
|
|
108
|
+
deadline="2026-05-13T08:00:00-04:00", # "by 8am tomorrow my time"
|
|
109
|
+
carbon_budget_g=5, # optional max grams CO2e
|
|
110
|
+
region="US-CAL-CISO", # optional grid region
|
|
111
|
+
)
|
|
112
|
+
print(result)
|
|
113
|
+
|
|
114
|
+
asyncio.run(main())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`defer()` returns whatever the callable returns. The task body can be
|
|
118
|
+
sync or async; both work.
|
|
119
|
+
|
|
120
|
+
### 2. Hand-built scheduler with SQLite persistence
|
|
121
|
+
|
|
122
|
+
For long-running services you want a single, explicitly-constructed
|
|
123
|
+
scheduler with a durable queue:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import asyncio
|
|
127
|
+
from ebb_ai import Scheduler, DeferOptions
|
|
128
|
+
|
|
129
|
+
async def main():
|
|
130
|
+
async with Scheduler(db_path="/var/lib/ebb/queue.sqlite") as scheduler:
|
|
131
|
+
await scheduler.defer(
|
|
132
|
+
lambda: do_research_run(),
|
|
133
|
+
DeferOptions(
|
|
134
|
+
deadline="2026-05-13T08:00:00-04:00",
|
|
135
|
+
carbon_budget_g=5,
|
|
136
|
+
region="US-CAL-CISO",
|
|
137
|
+
task_id="user:42:weekly-research",
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
asyncio.run(main())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If the process restarts, the next `Scheduler(db_path=…)` will see the
|
|
145
|
+
completed receipts via `await scheduler.list_persisted_tasks()`.
|
|
146
|
+
|
|
147
|
+
### 3. Anthropic Batch API
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
import asyncio
|
|
151
|
+
from ebb_ai import Scheduler, DeferOptions
|
|
152
|
+
from ebb_ai.providers import AnthropicAdapter
|
|
153
|
+
|
|
154
|
+
async def main():
|
|
155
|
+
adapter = AnthropicAdapter() # reads ANTHROPIC_API_KEY
|
|
156
|
+
|
|
157
|
+
async def work():
|
|
158
|
+
result = await adapter.dispatch(
|
|
159
|
+
"claude-sonnet-4-5",
|
|
160
|
+
"Summarize today's git commits.",
|
|
161
|
+
)
|
|
162
|
+
return result.text
|
|
163
|
+
|
|
164
|
+
async with Scheduler() as scheduler:
|
|
165
|
+
summary = await scheduler.defer(
|
|
166
|
+
work,
|
|
167
|
+
DeferOptions(
|
|
168
|
+
deadline="2026-05-13T08:00:00-04:00",
|
|
169
|
+
region="US-CAL-CISO",
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
print(summary)
|
|
173
|
+
|
|
174
|
+
asyncio.run(main())
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For genuinely batched work (50+ prompts at once), use the Batch API
|
|
178
|
+
directly:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
handle = await adapter.dispatch_batch(
|
|
182
|
+
"claude-sonnet-4-5",
|
|
183
|
+
["prompt 1", "prompt 2", "prompt 3"],
|
|
184
|
+
)
|
|
185
|
+
print(handle.batch_id) # poll via the Anthropic SDK
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 4. OpenAI Batch API
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import asyncio
|
|
192
|
+
from ebb_ai.providers import OpenAIAdapter
|
|
193
|
+
|
|
194
|
+
async def main():
|
|
195
|
+
adapter = OpenAIAdapter() # reads OPENAI_API_KEY
|
|
196
|
+
handle = await adapter.dispatch_batch(
|
|
197
|
+
"gpt-4.1-mini",
|
|
198
|
+
["prompt 1", "prompt 2"],
|
|
199
|
+
)
|
|
200
|
+
print(handle.batch_id)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`OpenAIAdapter` builds the required JSONL input file in memory,
|
|
204
|
+
uploads it via `files.create(purpose="batch")`, and submits the batch
|
|
205
|
+
job — the standard OpenAI batch flow.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## API reference
|
|
210
|
+
|
|
211
|
+
### Top-level exports
|
|
212
|
+
|
|
213
|
+
| Name | Kind | Description |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `defer(task, *, deadline, carbon_budget_g, region, task_id)` | async function | Defers on the process-wide default scheduler. |
|
|
216
|
+
| `Scheduler` | class | Explicit scheduler; supports SQLite persistence. |
|
|
217
|
+
| `mock_grid_feed()` | function | Deterministic synthetic feed for dev/tests. |
|
|
218
|
+
| `electricity_maps_feed(api_key=None)` | function | Electricity Maps free-tier API client. |
|
|
219
|
+
| `pick_best_window(entries, deadline)` | pure function | Picks the lowest-intensity entry inside `[now, deadline]`. |
|
|
220
|
+
| `normalize_deadline(d)` | function | Parses + validates a user-supplied deadline. |
|
|
221
|
+
| `CarbonBudgetExceededError` | exception | Raised when no candidate window meets the user budget. |
|
|
222
|
+
| `InvalidDeadlineError` | exception (`ValueError`) | Raised when the deadline is unparseable or in the past. |
|
|
223
|
+
| `TaskRecord`, `CarbonReceipt`, `GridForecast`, `GridForecastEntry`, `DeferOptions` | dataclasses | Public record types. |
|
|
224
|
+
|
|
225
|
+
### `Scheduler`
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
Scheduler(*, feed: GridFeed | None = None,
|
|
229
|
+
default_region: str = "US-CAL-CISO",
|
|
230
|
+
db_path: str | None = None)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Methods:
|
|
234
|
+
|
|
235
|
+
- `await scheduler.connect()` — opens the SQLite connection (no-op
|
|
236
|
+
when `db_path` is unset).
|
|
237
|
+
- `await scheduler.defer(task, opts)` — deferred await. Returns the
|
|
238
|
+
task's eventual result.
|
|
239
|
+
- `scheduler.enqueue(task, opts)` — fire-and-forget. Returns the
|
|
240
|
+
`TaskRecord` immediately with `status="queued"`.
|
|
241
|
+
- `scheduler.get_task(task_id)` / `scheduler.list_tasks()` — in-memory
|
|
242
|
+
snapshots.
|
|
243
|
+
- `await scheduler.load_persisted_task(task_id)` /
|
|
244
|
+
`await scheduler.list_persisted_tasks()` — reads from SQLite.
|
|
245
|
+
- `await scheduler.shutdown()` — cancels in-flight schedule tasks and
|
|
246
|
+
closes the DB. The class also supports `async with`.
|
|
247
|
+
|
|
248
|
+
### Carbon-receipt schema
|
|
249
|
+
|
|
250
|
+
Every completed task has a `receipt` field:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
@dataclass
|
|
254
|
+
class CarbonReceipt:
|
|
255
|
+
task_id: str
|
|
256
|
+
ran_at: str
|
|
257
|
+
region: str
|
|
258
|
+
estimated_carbon_g_co2: float
|
|
259
|
+
provider: str | None = None
|
|
260
|
+
model: str | None = None
|
|
261
|
+
duration_ms: float | None = None
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The `TaskRecord` also carries `intensity_source`: `"scored"` if the
|
|
265
|
+
receipt used the forecast entry the scheduler chose against,
|
|
266
|
+
`"current"` if we had to fall back to a freshly-fetched current-hour
|
|
267
|
+
intensity at dispatch time.
|
|
268
|
+
|
|
269
|
+
The receipt's `estimated_carbon_g_co2` is computed via
|
|
270
|
+
`estimate_energy_kwh(model=..., input_tokens=..., output_tokens=...)
|
|
271
|
+
* grid_intensity_g_co2_per_kwh`. As of v0.6 the energy module ships
|
|
272
|
+
per-model Wh/token coefficients from the published research
|
|
273
|
+
(Patterson et al. 2021; Luccioni, Jernite, Strubell 2024; Hugging
|
|
274
|
+
Face AI Energy Score). When no model is specified the function
|
|
275
|
+
returns the pre-v0.6 flat `0.0015` kWh estimate to preserve
|
|
276
|
+
backwards compatibility. See `ebb_ai.energy` for the table and
|
|
277
|
+
citation metadata.
|
|
278
|
+
|
|
279
|
+
### Grid feeds
|
|
280
|
+
|
|
281
|
+
Both feeds implement `GridFeed.fetch_forecast(region, hours)`:
|
|
282
|
+
|
|
283
|
+
- `mock_grid_feed()` — deterministic intraday sinusoid with regional
|
|
284
|
+
floors. Useful for dev and CI without an API key.
|
|
285
|
+
- `electricity_maps_feed(api_key=None)` — hits
|
|
286
|
+
`api.electricitymap.org/v3/carbon-intensity/forecast` with a 5 s
|
|
287
|
+
hard timeout. Falls back to the mock feed (and logs a warning) on
|
|
288
|
+
any failure, mirroring the TypeScript port.
|
|
289
|
+
|
|
290
|
+
Supported region codes match Electricity Maps' zone codes
|
|
291
|
+
(`US-CAL-CISO`, `US-TEX-ERCO`, `US-NE-ISNE`, `US-NY-NYIS`,
|
|
292
|
+
`US-MIDA-PJM`, `US-MIDW-MISO`, `FR`, `DE`, `GB`, ...).
|
|
293
|
+
|
|
294
|
+
### Provider adapters
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from ebb_ai.providers import AnthropicAdapter, OpenAIAdapter, DispatchOptions
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Both adapters implement:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
async def dispatch(model: str, prompt: str,
|
|
304
|
+
options: DispatchOptions | None = None) -> DispatchResult: ...
|
|
305
|
+
|
|
306
|
+
async def dispatch_batch(model: str, prompts: list[str],
|
|
307
|
+
options: DispatchOptions | None = None) -> BatchHandle: ...
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Both modules import cleanly even when the vendor SDK isn't installed —
|
|
311
|
+
construction is what raises a clear error. This means you can write
|
|
312
|
+
code that *references* the adapters without forcing a dependency on
|
|
313
|
+
both SDKs.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Persistence (Python-only in v0.2)
|
|
318
|
+
|
|
319
|
+
The Python port ships SQLite-backed durability from day one. To enable
|
|
320
|
+
it, construct the `Scheduler` with a `db_path`:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
async with Scheduler(db_path="/var/lib/ebb/queue.sqlite") as scheduler:
|
|
324
|
+
...
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Schema:
|
|
328
|
+
|
|
329
|
+
```sql
|
|
330
|
+
CREATE TABLE tasks (
|
|
331
|
+
task_id TEXT PRIMARY KEY,
|
|
332
|
+
status TEXT NOT NULL,
|
|
333
|
+
enqueued_at TEXT NOT NULL,
|
|
334
|
+
scheduled_for TEXT,
|
|
335
|
+
completed_at TEXT,
|
|
336
|
+
region TEXT NOT NULL,
|
|
337
|
+
carbon_budget_g REAL,
|
|
338
|
+
result_json TEXT,
|
|
339
|
+
error TEXT,
|
|
340
|
+
receipt_json TEXT,
|
|
341
|
+
intensity_source TEXT
|
|
342
|
+
);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Receipts and results are stored as JSON. Non-JSON results (e.g. an
|
|
346
|
+
Anthropic `Message` object) are stored as `{"_repr": "<repr>"}` so
|
|
347
|
+
audit trails survive odd value types.
|
|
348
|
+
|
|
349
|
+
Multi-writer support (multiple schedulers pointed at one DB) requires
|
|
350
|
+
WAL mode and is on the v0.3 roadmap.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Testing
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
cd packages/core-py
|
|
358
|
+
python3 -m venv .venv
|
|
359
|
+
. .venv/bin/activate
|
|
360
|
+
pip install -e ".[dev,anthropic,openai]"
|
|
361
|
+
pytest -v
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The test suite covers:
|
|
365
|
+
|
|
366
|
+
- Mock grid feed shape and intraday curve.
|
|
367
|
+
- `pick_best_window` correctness (empty, past, in-window).
|
|
368
|
+
- Scheduler accounting: queued/scheduled/running/completed
|
|
369
|
+
transitions.
|
|
370
|
+
- Deadline validation: unparseable strings, past dates,
|
|
371
|
+
`datetime` instances.
|
|
372
|
+
- Carbon-budget enforcement (US-MIDW-MISO floor exceeds a 0.1 g cap).
|
|
373
|
+
- SQLite round-trips, including reopening the DB after shutdown.
|
|
374
|
+
- Provider adapter request shapes and response parsing for both
|
|
375
|
+
Anthropic and OpenAI (mocked SDKs — no live calls).
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Roadmap
|
|
380
|
+
|
|
381
|
+
- **v0.2 (this release)** — scheduler, grid feeds, SQLite persistence,
|
|
382
|
+
Anthropic + OpenAI adapters with Batch API.
|
|
383
|
+
- **v0.3** — per-model energy coefficients; WattTime marginal-emissions
|
|
384
|
+
feed; multi-writer DB with WAL mode; richer receipt fields (model,
|
|
385
|
+
provider, token counts) propagated by the scheduler itself.
|
|
386
|
+
- **v0.4** — Gemini adapter; local-Ollama route; MCP spec PRs for
|
|
387
|
+
`priority` / `deadline` / `carbon_budget` fields.
|
|
388
|
+
|
|
389
|
+
See [`ROADMAP.md`](../../ROADMAP.md) for the full 24-week plan.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
[Apache License 2.0](../../LICENSE) — patent grant included.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
*Built by [Vitalii Borovyk](https://github.com/Vitalini).*
|