offpeak 0.2.0__tar.gz → 0.2.2__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.
Files changed (50) hide show
  1. offpeak-0.2.2/.github/workflows/docs.yml +54 -0
  2. {offpeak-0.2.0 → offpeak-0.2.2}/.github/workflows/nightly.yml +14 -1
  3. offpeak-0.2.2/.github/workflows/settle.yml +56 -0
  4. {offpeak-0.2.0 → offpeak-0.2.2}/.gitignore +4 -0
  5. {offpeak-0.2.0 → offpeak-0.2.2}/PKG-INFO +22 -9
  6. {offpeak-0.2.0 → offpeak-0.2.2}/README.md +15 -8
  7. offpeak-0.2.2/docs/index.md +39 -0
  8. offpeak-0.2.2/docs/night-board.md +123 -0
  9. offpeak-0.2.2/docs/quickstart.md +152 -0
  10. offpeak-0.2.2/docs/reference.md +48 -0
  11. offpeak-0.2.2/docs/roadmap.md +54 -0
  12. offpeak-0.2.2/mkdocs.yml +56 -0
  13. offpeak-0.2.2/mkdocs_hooks.py +21 -0
  14. {offpeak-0.2.0 → offpeak-0.2.2}/pyproject.toml +3 -1
  15. offpeak-0.2.2/receipts/2026-08-22-mechanics-1.json +32 -0
  16. offpeak-0.2.2/receipts/2026-08-22-mechanics-2.json +28 -0
  17. offpeak-0.2.2/receipts/2026-08-22-mechanics-3.json +27 -0
  18. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/__init__.py +1 -1
  19. offpeak-0.2.2/src/offpeak/prices.py +251 -0
  20. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/quote.py +83 -8
  21. offpeak-0.2.2/src/offpeak/venues/groq_batch.py +122 -0
  22. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/venues/openai_batch.py +30 -3
  23. offpeak-0.2.2/tests/test_groq_venue.py +146 -0
  24. offpeak-0.2.2/tests/test_mechanics_run.py +133 -0
  25. offpeak-0.2.2/tests/test_night_report.py +488 -0
  26. offpeak-0.2.2/tests/test_prices.py +155 -0
  27. {offpeak-0.2.0 → offpeak-0.2.2}/tests/test_quote.py +82 -6
  28. {offpeak-0.2.0 → offpeak-0.2.2}/tests/test_run.py +71 -1
  29. offpeak-0.2.2/tests/test_settle_report.py +127 -0
  30. offpeak-0.2.2/tools/mechanics_run.py +295 -0
  31. offpeak-0.2.2/tools/night_report.py +610 -0
  32. offpeak-0.2.2/tools/settle_report.py +144 -0
  33. offpeak-0.2.0/src/offpeak/prices.py +0 -98
  34. offpeak-0.2.0/tests/test_night_report.py +0 -178
  35. offpeak-0.2.0/tools/night_report.py +0 -283
  36. {offpeak-0.2.0 → offpeak-0.2.2}/.github/workflows/ci.yml +0 -0
  37. {offpeak-0.2.0 → offpeak-0.2.2}/.github/workflows/publish.yml +0 -0
  38. {offpeak-0.2.0 → offpeak-0.2.2}/CONTRIBUTING.md +0 -0
  39. {offpeak-0.2.0 → offpeak-0.2.2}/LICENSE +0 -0
  40. {offpeak-0.2.0 → offpeak-0.2.2}/SPEC.md +0 -0
  41. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/__main__.py +0 -0
  42. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/client.py +0 -0
  43. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/deadline.py +0 -0
  44. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/job.py +0 -0
  45. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/venues/__init__.py +0 -0
  46. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/venues/anthropic_batch.py +0 -0
  47. {offpeak-0.2.0 → offpeak-0.2.2}/src/offpeak/venues/base.py +0 -0
  48. {offpeak-0.2.0 → offpeak-0.2.2}/tests/conftest.py +0 -0
  49. {offpeak-0.2.0 → offpeak-0.2.2}/tests/test_deadline.py +0 -0
  50. {offpeak-0.2.0 → offpeak-0.2.2}/tests/test_job_receipt.py +0 -0
@@ -0,0 +1,54 @@
1
+ name: Docs
2
+
3
+ # Builds the mkdocs site and deploys it to GitHub Pages. Pages is configured
4
+ # with build_type=workflow, so this workflow is the only publisher — there is
5
+ # no gh-pages branch to keep in sync.
6
+
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ paths:
11
+ - "docs/**"
12
+ - "src/**"
13
+ - "mkdocs.yml"
14
+ - "mkdocs_hooks.py"
15
+ - "SPEC.md"
16
+ - "pyproject.toml"
17
+ - ".github/workflows/docs.yml"
18
+ workflow_dispatch:
19
+
20
+ permissions:
21
+ contents: read
22
+ pages: write
23
+ id-token: write
24
+
25
+ concurrency:
26
+ group: pages
27
+ cancel-in-progress: false
28
+
29
+ jobs:
30
+ build:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v7
34
+ - uses: actions/setup-python@v7
35
+ with:
36
+ python-version: "3.12"
37
+ - name: Install
38
+ run: pip install -e ".[docs]"
39
+ - name: Build
40
+ run: mkdocs build --strict
41
+ - uses: actions/configure-pages@v6
42
+ - uses: actions/upload-pages-artifact@v5
43
+ with:
44
+ path: site
45
+
46
+ deploy:
47
+ needs: build
48
+ runs-on: ubuntu-latest
49
+ environment:
50
+ name: github-pages
51
+ url: ${{ steps.deployment.outputs.page_url }}
52
+ steps:
53
+ - id: deployment
54
+ uses: actions/deploy-pages@v5
@@ -20,8 +20,11 @@ on:
20
20
  permissions:
21
21
  contents: write
22
22
 
23
+ # Everything that pushes board-data shares one queue. Two workflows writing
24
+ # different files on the same branch still race at the push, and a rejected
25
+ # push loses a night's mark.
23
26
  concurrency:
24
- group: night-board
27
+ group: board-data
25
28
  cancel-in-progress: false
26
29
 
27
30
  jobs:
@@ -54,10 +57,20 @@ jobs:
54
57
  echo "mode=$mode" >> "$GITHUB_OUTPUT"
55
58
  echo "running the $mode pass"
56
59
 
60
+ - name: Install US-zone data source
61
+ # gridstatus is an Action-only dependency: the SDK does not need it and
62
+ # the generator degrades to GB-only without it.
63
+ run: pip install "gridstatus>=0.36"
64
+
57
65
  - name: Generate
66
+ # EIA_API_KEY is optional: absent, the US carbon columns are recorded
67
+ # unavailable and every other leg runs exactly as before.
68
+ env:
69
+ EIA_API_KEY: ${{ secrets.EIA_API_KEY }}
58
70
  run: |
59
71
  python tools/night_report.py \
60
72
  --mode "${{ steps.pick.outputs.mode }}" \
73
+ --us-zones \
61
74
  --outdir board/nightly
62
75
 
63
76
  - name: Commit to board-data
@@ -0,0 +1,56 @@
1
+ name: Settled runs
2
+
3
+ # Publishes the receipts in `receipts/` onto the board-data branch as
4
+ # `nightly/SETTLED.md`. Manual only: a settlement is a deliberate act, and the
5
+ # ledger should move when someone decides it moves, not on every push.
6
+ #
7
+ # board-data is written by CI and never by hand — the same rule the night board
8
+ # follows, for the same reason: main is protected and a bot push there would be
9
+ # rejected, and a ledger anyone can hand-edit is not a ledger.
10
+
11
+ on:
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: write
16
+
17
+ # Shared with the night board: everything that pushes board-data queues here.
18
+ concurrency:
19
+ group: board-data
20
+ cancel-in-progress: false
21
+
22
+ jobs:
23
+ settle:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - name: Check out the generator and the receipts (main)
27
+ uses: actions/checkout@v7
28
+
29
+ - uses: actions/setup-python@v7
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ - name: Check out the ledger (board-data)
34
+ uses: actions/checkout@v7
35
+ with:
36
+ ref: board-data
37
+ path: board
38
+
39
+ - name: Render
40
+ run: |
41
+ python tools/settle_report.py \
42
+ --receipts receipts \
43
+ --outdir board/nightly
44
+
45
+ - name: Commit to board-data
46
+ working-directory: board
47
+ run: |
48
+ git config user.name "github-actions[bot]"
49
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
50
+ git add nightly
51
+ if git diff --staged --quiet; then
52
+ echo "nothing changed — the ledger already says this"
53
+ exit 0
54
+ fi
55
+ git commit -m "board: settled runs $(date -u +%Y-%m-%d)"
56
+ git push
@@ -23,3 +23,7 @@ htmlcov/
23
23
  # Claude Code (personal)
24
24
  .claude/settings.local.json
25
25
  HANDOFF.md
26
+
27
+ # Docs build output; docs/spec.md is copied from SPEC.md at build time
28
+ site/
29
+ docs/spec.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: offpeak
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Deadline-priced inference: give AI jobs a deadline and run them on the cheapest venue — provider batch tiers (−50%) today. Same model, same tokens, a different hour.
5
5
  Project-URL: Homepage, https://github.com/offpeak-ai/offpeak
6
6
  Project-URL: Repository, https://github.com/offpeak-ai/offpeak
@@ -31,6 +31,12 @@ Requires-Dist: build; extra == 'dev'
31
31
  Requires-Dist: pytest>=8; extra == 'dev'
32
32
  Requires-Dist: ruff>=0.6; extra == 'dev'
33
33
  Requires-Dist: twine; extra == 'dev'
34
+ Provides-Extra: docs
35
+ Requires-Dist: mkdocs-material<10,>=9.5; extra == 'docs'
36
+ Requires-Dist: mkdocs<2,>=1.6; extra == 'docs'
37
+ Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
38
+ Provides-Extra: groq
39
+ Requires-Dist: groq>=0.11; extra == 'groq'
34
40
  Provides-Extra: openai
35
41
  Requires-Dist: openai>=1.50; extra == 'openai'
36
42
  Description-Content-Type: text/markdown
@@ -66,10 +72,12 @@ tokens 12,410,332 in · 3,104,551 out
66
72
  list $27.93
67
73
  paid $14.02
68
74
  captured $13.91 (49.8%)
69
- prices snapshot 2026-08 — override via offpeak.prices
75
+ prices snapshot 2026-08-21 — override via offpeak.prices
70
76
  ───────────────────────────────────────────────
71
77
  ```
72
78
 
79
+ **[Documentation](https://offpeak-ai.github.io/offpeak/)** · [Quickstart](https://offpeak-ai.github.io/offpeak/quickstart/) · [Spec](https://offpeak-ai.github.io/offpeak/spec/) · [Roadmap](https://offpeak-ai.github.io/offpeak/roadmap/)
80
+
73
81
  ## What it does
74
82
 
75
83
  - **Know the price before you spend it.** `quote(jobs, deadline=...)` prices a run against the published sheets with no API calls and no key — list versus batch, per venue, plus what the wait is worth.
@@ -103,18 +111,21 @@ jobs 5000 across 1 venue(s)
103
111
  deadline 2026-08-21 21:11 PDT (24.0h out)
104
112
  tokens 4,000,000 in · 1,000,000 out
105
113
 
106
- openai:batch 5000 job(s) list $1.00 batch $0.50 save $0.50 (50.0%)
114
+ openai:batch 5000 job(s) list $2.00 batch $1.00 save $1.00 (50.0%)
107
115
 
108
- list $1.00 (run now, synchronously)
109
- batch $0.50 (run by the deadline)
110
- save $0.50 (50.0%)
116
+ list $2.00 (run now, synchronously)
117
+ batch $1.00 (run by the deadline)
118
+ save $1.00 (50.0%)
119
+ risk deadline is inside the 24h batch window — the SLA rests on the sync fallback, which pays list
111
120
  basis input explicit; output explicit
112
- prices snapshot 2026-08 — estimate only, not a bill
121
+ prices snapshot 2026-08-21 — estimate only, not a bill
113
122
  ───────────────────────────────────────────────
114
123
  ```
115
124
 
116
125
  From Python, `offpeak.quote(jobs, deadline="06:00")` takes the same jobs you would pass to `run()`. Token counts come from the job where it knows them (`metadata={"input_tokens": ..., "output_tokens": ...}`, or `max_tokens` as an output ceiling) and are a labeled chars/4 estimate where it does not — every figure reports its provenance in `basis`, and a quote with no output signal is marked a **floor**, not an estimate.
117
126
 
127
+ If you do know roughly what the model will write, say so and get a priced number instead — `quote(jobs, deadline=..., assumed_output_ratio=0.25)`, or `metadata={"expected_output_tokens": 300}` on a single job. Those quotes are marked **EST**, distinct from a floor. Both are opt-in: absent one, `offpeak` assumes nothing on your behalf.
128
+
118
129
  ## Deadlines
119
130
 
120
131
  Deadlines are how software says "this can wait" — the full semantics live in [SPEC.md](SPEC.md).
@@ -152,13 +163,15 @@ import offpeak
152
163
  offpeak.prices.register_price("my-fine-tune", input_per_m=4.0, output_per_m=16.0)
153
164
  ```
154
165
 
155
- Unknown models settle with `cost = None` rather than a guess.
166
+ Unknown models settle with `cost = None` rather than a guess. The sheet also carries what the venues charge for *urgency* — `get_fast_price()`, `urgency_spread()` — and flags list prices that are promotional, with the date and the price they decay to: `promo_decay("gpt-5.6-sol")` is `(1.25, 1.5)` after 2026-11-21.
156
167
 
157
168
  ## What this is (and the roadmap)
158
169
 
159
170
  `offpeak` is the open client and spec for a simple claim: **intelligence has a time value**. A large share of AI work — embeddings, evals, backfills, report generation, overnight agents — has no human waiting on it, and the venues already price that patience at −50%. This library is the missing workflow.
160
171
 
161
- The **[night board](https://github.com/offpeak-ai/offpeak/blob/board-data/nightly/BOARD.md)** marks the same claim against open grid data every night — power and carbon peak/off-peak spreads, alongside the 2.0x token spread the batch tiers already publish.
172
+ The token side is wider than the headline discount. Patience is priced at −50% — a 2.0x spread — and haste is priced too: hold the model and venue constant and `gpt-5.6-sol` costs **$8.00 / $40.00** per 1M on OpenAI's fast tier against **$2.00 / $10.00** on its batch tier, a **4x intra-venue urgency spread** for the hour alone ([source](https://developers.openai.com/api/docs/pricing); sol's standard rate is promotional at least through 2026-11-21, and both tiers are defined off it, so the ratio outlives the prices). It is data, not prose: `offpeak.prices.urgency_spread("gpt-5.6-sol")` returns `4.0`.
173
+
174
+ The **[night board](https://github.com/offpeak-ai/offpeak/blob/board-data/nightly/BOARD.md)** marks the same claim against open grid data every night — GB power and carbon plus CAISO SP15 and ERCOT Houston peak/off-peak spreads, alongside the published token spreads. ERCOT Houston marked 3.94x on the night of 2026-08-20; a venue charges 4x for the same impatience.
162
175
 
163
176
  The roadmap follows the same interface upward: more venues (Google batch, spot capacity, off-peak windows on your own GPUs), queue-latency forecasting instead of a fixed risk buffer, portfolio placement across venues, energy- and carbon-aware scheduling with per-job receipts. The venue interface (`offpeak.Venue`) is deliberately the extension point — a venue is anywhere deferred work can run.
164
177
 
@@ -29,10 +29,12 @@ tokens 12,410,332 in · 3,104,551 out
29
29
  list $27.93
30
30
  paid $14.02
31
31
  captured $13.91 (49.8%)
32
- prices snapshot 2026-08 — override via offpeak.prices
32
+ prices snapshot 2026-08-21 — override via offpeak.prices
33
33
  ───────────────────────────────────────────────
34
34
  ```
35
35
 
36
+ **[Documentation](https://offpeak-ai.github.io/offpeak/)** · [Quickstart](https://offpeak-ai.github.io/offpeak/quickstart/) · [Spec](https://offpeak-ai.github.io/offpeak/spec/) · [Roadmap](https://offpeak-ai.github.io/offpeak/roadmap/)
37
+
36
38
  ## What it does
37
39
 
38
40
  - **Know the price before you spend it.** `quote(jobs, deadline=...)` prices a run against the published sheets with no API calls and no key — list versus batch, per venue, plus what the wait is worth.
@@ -66,18 +68,21 @@ jobs 5000 across 1 venue(s)
66
68
  deadline 2026-08-21 21:11 PDT (24.0h out)
67
69
  tokens 4,000,000 in · 1,000,000 out
68
70
 
69
- openai:batch 5000 job(s) list $1.00 batch $0.50 save $0.50 (50.0%)
71
+ openai:batch 5000 job(s) list $2.00 batch $1.00 save $1.00 (50.0%)
70
72
 
71
- list $1.00 (run now, synchronously)
72
- batch $0.50 (run by the deadline)
73
- save $0.50 (50.0%)
73
+ list $2.00 (run now, synchronously)
74
+ batch $1.00 (run by the deadline)
75
+ save $1.00 (50.0%)
76
+ risk deadline is inside the 24h batch window — the SLA rests on the sync fallback, which pays list
74
77
  basis input explicit; output explicit
75
- prices snapshot 2026-08 — estimate only, not a bill
78
+ prices snapshot 2026-08-21 — estimate only, not a bill
76
79
  ───────────────────────────────────────────────
77
80
  ```
78
81
 
79
82
  From Python, `offpeak.quote(jobs, deadline="06:00")` takes the same jobs you would pass to `run()`. Token counts come from the job where it knows them (`metadata={"input_tokens": ..., "output_tokens": ...}`, or `max_tokens` as an output ceiling) and are a labeled chars/4 estimate where it does not — every figure reports its provenance in `basis`, and a quote with no output signal is marked a **floor**, not an estimate.
80
83
 
84
+ If you do know roughly what the model will write, say so and get a priced number instead — `quote(jobs, deadline=..., assumed_output_ratio=0.25)`, or `metadata={"expected_output_tokens": 300}` on a single job. Those quotes are marked **EST**, distinct from a floor. Both are opt-in: absent one, `offpeak` assumes nothing on your behalf.
85
+
81
86
  ## Deadlines
82
87
 
83
88
  Deadlines are how software says "this can wait" — the full semantics live in [SPEC.md](SPEC.md).
@@ -115,13 +120,15 @@ import offpeak
115
120
  offpeak.prices.register_price("my-fine-tune", input_per_m=4.0, output_per_m=16.0)
116
121
  ```
117
122
 
118
- Unknown models settle with `cost = None` rather than a guess.
123
+ Unknown models settle with `cost = None` rather than a guess. The sheet also carries what the venues charge for *urgency* — `get_fast_price()`, `urgency_spread()` — and flags list prices that are promotional, with the date and the price they decay to: `promo_decay("gpt-5.6-sol")` is `(1.25, 1.5)` after 2026-11-21.
119
124
 
120
125
  ## What this is (and the roadmap)
121
126
 
122
127
  `offpeak` is the open client and spec for a simple claim: **intelligence has a time value**. A large share of AI work — embeddings, evals, backfills, report generation, overnight agents — has no human waiting on it, and the venues already price that patience at −50%. This library is the missing workflow.
123
128
 
124
- The **[night board](https://github.com/offpeak-ai/offpeak/blob/board-data/nightly/BOARD.md)** marks the same claim against open grid data every night — power and carbon peak/off-peak spreads, alongside the 2.0x token spread the batch tiers already publish.
129
+ The token side is wider than the headline discount. Patience is priced at −50% — a 2.0x spread — and haste is priced too: hold the model and venue constant and `gpt-5.6-sol` costs **$8.00 / $40.00** per 1M on OpenAI's fast tier against **$2.00 / $10.00** on its batch tier, a **4x intra-venue urgency spread** for the hour alone ([source](https://developers.openai.com/api/docs/pricing); sol's standard rate is promotional at least through 2026-11-21, and both tiers are defined off it, so the ratio outlives the prices). It is data, not prose: `offpeak.prices.urgency_spread("gpt-5.6-sol")` returns `4.0`.
130
+
131
+ The **[night board](https://github.com/offpeak-ai/offpeak/blob/board-data/nightly/BOARD.md)** marks the same claim against open grid data every night — GB power and carbon plus CAISO SP15 and ERCOT Houston peak/off-peak spreads, alongside the published token spreads. ERCOT Houston marked 3.94x on the night of 2026-08-20; a venue charges 4x for the same impatience.
125
132
 
126
133
  The roadmap follows the same interface upward: more venues (Google batch, spot capacity, off-peak windows on your own GPUs), queue-latency forecasting instead of a fixed risk buffer, portfolio placement across venues, energy- and carbon-aware scheduling with per-job receipts. The venue interface (`offpeak.Venue`) is deliberately the extension point — a venue is anywhere deferred work can run.
127
134
 
@@ -0,0 +1,39 @@
1
+ # offpeak
2
+
3
+ **Deadline-priced inference.** Same model, same tokens, a different hour.
4
+
5
+ A large share of AI work — embeddings, evals, backfills, report generation,
6
+ overnight agents — has no human waiting on it. The providers already price that
7
+ patience: OpenAI and Anthropic both publish their batch tiers at **50% of
8
+ list**. `offpeak` is the workflow that collects the difference.
9
+
10
+ ```python
11
+ import offpeak
12
+
13
+ jobs = [offpeak.job("claude-haiku-4-5", f"Summarize:\n\n{d}") for d in docs]
14
+
15
+ print(offpeak.quote(jobs, deadline="06:00")) # what is the wait worth?
16
+ results = offpeak.run(jobs, deadline="06:00") # collect it
17
+ print(offpeak.receipt(results)) # what it actually cost
18
+ ```
19
+
20
+ - **[Quickstart](quickstart.md)** — install, quote, run, read the receipt.
21
+ - **[The night board](night-board.md)** — the same claim, marked nightly against open grid data.
22
+ - **[Spec](spec.md)** — deadline semantics, statuses, receipts.
23
+ - **[API reference](reference.md)** — every public symbol.
24
+ - **[Roadmap](roadmap.md)** — what exists, what does not, and what is being built.
25
+
26
+ ## What it guarantees
27
+
28
+ **One `Result` per job, always.** Provider failures at submit, poll, cancel or
29
+ sync are captured, not raised. Affected jobs take the sync fallback where the
30
+ deadline still allows it, and otherwise return failed with the provider's
31
+ message attached. Exceptions are reserved for programming errors — a deadline
32
+ in the past, or a model no venue supports.
33
+
34
+ **Your keys, your perimeter.** `offpeak` talks straight to the providers with
35
+ your own credentials. There is no proxy and no third party in the data path.
36
+
37
+ **Receipts are arithmetic, not estimates.** Every figure traces to a published
38
+ price sheet, and a model that is not on one settles as `None` rather than a
39
+ guess.
@@ -0,0 +1,123 @@
1
+ # The night board
2
+
3
+ `offpeak` rests on a claim: **intelligence has a time value.** The token side of
4
+ that claim is already settled, and it is wider than the headline discount.
5
+
6
+ - **Patience is priced at −50%.** OpenAI and Anthropic both publish batch tiers
7
+ at half of list: a flat **2.0x** spread for work that can wait.
8
+ - **Haste is priced too.** Hold the model and the venue constant and read the
9
+ same sheet across its urgency tiers: `gpt-5.6-sol` is **$8.00 / $40.00** per
10
+ 1M tokens on OpenAI's fast tier against **$2.00 / $10.00** on its batch tier
11
+ — a **4x intra-venue urgency spread**, on both legs, for the hour alone.
12
+ Source: [developers.openai.com/api/docs/pricing](https://developers.openai.com/api/docs/pricing).
13
+
14
+ !!! note "The promo caveat"
15
+ `gpt-5.6-sol`'s standard rate is promotional — the sheet says it runs *at
16
+ least* through **2026-11-21**, after which list is $5/$30. Fast and batch
17
+ are both defined off that list (2x and 0.5x), so the promo moves the
18
+ dollars and leaves the ratio: **the 4x is the durable figure, the prices
19
+ are the perishable ones.** Both are in the SDK rather than in prose —
20
+ `offpeak.prices.urgency_spread("gpt-5.6-sol")` returns `4.0`, and
21
+ `promo_decay()` returns the step-up the date will bring.
22
+
23
+ The night board marks the same claim against the other side of the trade: the
24
+ grid the compute runs on. Every night it records what power and carbon actually
25
+ did between the evening peak and the small hours, from open, keyless sources.
26
+ The grid's spread is not published anywhere — it has to be observed — and on the
27
+ night of 2026-08-20 the ERCOT Houston hub marked **3.94x** between its evening
28
+ peak and the trough that followed, against the 4x a venue charges for the same
29
+ hour of impatience.
30
+
31
+ The two sides of the grid do not move together, which is the point of marking
32
+ both. On 2026-08-18 and 08-19, CAISO's carbon ran **cleaner at the evening peak
33
+ than in the small hours** — spreads of 0.76x and 0.73x — because the sun that
34
+ serves the California evening has set by midnight. Cheap hours are not
35
+ automatically clean hours, and a board that only recorded price would have
36
+ implied otherwise.
37
+
38
+ **[→ Read the board](https://github.com/offpeak-ai/offpeak/blob/board-data/nightly/BOARD.md)**
39
+
40
+ ## How it works
41
+
42
+ Two passes over the same night, run by
43
+ [a scheduled workflow](https://github.com/offpeak-ai/offpeak/blob/main/.github/workflows/nightly.yml):
44
+
45
+ | Pass | When | What it records |
46
+ |---|---|---|
47
+ | `quote` | 19:00Z | The night ahead — carbon **forecast**, day-ahead power |
48
+ | `mark` | 06:30Z | The night just finished — carbon **actuals** |
49
+
50
+ A night runs **16:00Z–07:00Z**: it opens with the 17:00 BST evening peak and
51
+ closes after the 00–05 BST trough, so one span carries both windows the board
52
+ compares.
53
+
54
+ Output lands on the
55
+ [`board-data` branch](https://github.com/offpeak-ai/offpeak/tree/board-data) —
56
+ `nightly/BOARD.md` plus the raw JSON per night — because `main` is protected and
57
+ would reject a nightly bot push.
58
+
59
+ ## Settled runs are a different ledger
60
+
61
+ `BOARD.md` observes; it spends nothing at any venue. Runs that actually
62
+ executed and actually billed go in `nightly/SETTLED.md` on the same branch,
63
+ written by
64
+ [`tools/settle_report.py`](https://github.com/offpeak-ai/offpeak/blob/main/tools/settle_report.py)
65
+ from the receipts in
66
+ [`receipts/`](https://github.com/offpeak-ai/offpeak/tree/main/receipts) — never
67
+ by hand — and published by a manual workflow, because a settlement is a
68
+ deliberate act.
69
+
70
+ Every settled row carries its **scale**, and the column is not decoration. A
71
+ few dozen jobs proving the mechanics end to end and a production book are both
72
+ real settlements and are not the same evidence. A ledger that lets a reader
73
+ confuse them is doing marketing rather than accounting, so the scale is printed
74
+ before the money is.
75
+
76
+ ## Sources
77
+
78
+ - **Carbon** — [NESO carbon intensity](https://api.carbonintensity.org.uk),
79
+ GB, keyless.
80
+ - **Power, GB** — [Octopus Agile](https://api.octopus.energy) day-ahead unit
81
+ rates, GB region C, keyless.
82
+ - **Power, US** — CAISO SP15 and ERCOT Houston day-ahead hourly, via
83
+ [gridstatus](https://github.com/gridstatus/gridstatus), keyless.
84
+ - **Carbon, US** — [EIA-930](https://www.eia.gov/electricity/gridmonitor/)
85
+ hourly generation by fuel for the CAISO and ERCOT balancing authorities,
86
+ through the EIA Hourly Grid Monitor. Needs a free API key; without one the
87
+ column records itself unavailable and nothing else changes.
88
+ - **Tokens** — the published price sheets, not a measurement:
89
+ [OpenAI](https://developers.openai.com/api/docs/pricing) and
90
+ [Anthropic](https://platform.claude.com/docs/en/about-claude/pricing).
91
+
92
+ All are public and free. The board costs nothing to run and spends nothing at
93
+ any venue.
94
+
95
+ ## Honest limits
96
+
97
+ - **Four zones, unevenly covered.** GB carbon and GB power are half-hourly and
98
+ complete. CAISO SP15 and ERCOT Houston are **day-ahead hourly** prices, not
99
+ settled real-time ones.
100
+ - **US carbon is derived, GB carbon is measured.** NESO publishes an intensity;
101
+ EIA does not. The US columns are computed from EIA-930's hourly generation
102
+ mix times EIA's own CO2 coefficients and fleet heat rates — every input
103
+ published, the product an estimate, and marked `"basis": "derived"` in the
104
+ record so it is never confused with a measurement. It counts generation, not
105
+ consumption: imports and the carbon already stored in a battery are outside
106
+ what the method can see, and the share of generation EIA files under "other"
107
+ is reported per night rather than averaged in.
108
+ - **EIA runs about a day behind.** The 06:30Z mark usually lands before EIA has
109
+ published the night it is marking, so the US carbon columns are often empty
110
+ at first sight and fill in on a later re-mark. A column that is not there yet
111
+ is recorded as unavailable, never as zero.
112
+ - **The token column is published, not observed.** The 2.0x and the 4x are read
113
+ off price sheets; only the grid columns are measurements. A published number
114
+ and a marked one are different kinds of claim, and the board should not blur
115
+ them.
116
+ - **Observation, not advice.** The board records what the grid did. It does not
117
+ forecast, and `offpeak` does not currently schedule against it — the SDK
118
+ routes on published token prices alone.
119
+ - **Carbon actuals lag** roughly two hours. The 06:30Z mark clears the 00–05 BST
120
+ trough comfortably; the tail of the night can still be sparse.
121
+ - **A dead source costs its column, not the run.** Legs degrade independently,
122
+ and a night where both sources are down is recorded as unavailable rather than
123
+ guessed at.
@@ -0,0 +1,152 @@
1
+ # Quickstart
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ pip install "offpeak[all]" # OpenAI + Anthropic venues
7
+ pip install "offpeak[anthropic]" # or just one
8
+ pip install "offpeak[openai]"
9
+ ```
10
+
11
+ The core has zero dependencies; provider SDKs load only through the extras.
12
+ Venues read the standard environment variables (`OPENAI_API_KEY`,
13
+ `ANTHROPIC_API_KEY`), or take a configured client:
14
+ `OpenAIBatch(client=my_client)`.
15
+
16
+ ## 1. Quote — before you spend anything
17
+
18
+ `quote()` makes **no API calls and needs no key**. It prices your jobs against
19
+ the bundled sheet: list versus batch, per venue.
20
+
21
+ ```bash
22
+ python -m offpeak quote --model gpt-5.6-luna --input-tokens 800 --output-tokens 200 --jobs 5000
23
+ ```
24
+
25
+ ```
26
+ OFFPEAK QUOTE ─────────────────────────────────
27
+ jobs 5000 across 1 venue(s)
28
+ deadline 2026-08-21 21:11 PDT (24.0h out)
29
+ tokens 4,000,000 in · 1,000,000 out
30
+
31
+ openai:batch 5000 job(s) list $2.00 batch $1.00 save $1.00 (50.0%)
32
+
33
+ list $2.00 (run now, synchronously)
34
+ batch $1.00 (run by the deadline)
35
+ save $1.00 (50.0%)
36
+ risk deadline is inside the 24h batch window — the SLA rests on the sync fallback, which pays list
37
+ basis input explicit; output explicit
38
+ prices snapshot 2026-08-21 — estimate only, not a bill
39
+ ───────────────────────────────────────────────
40
+ ```
41
+
42
+ From Python, the same jobs you would pass to `run()`:
43
+
44
+ ```python
45
+ q = offpeak.quote(jobs, deadline="06:00")
46
+ print(q.spread_usd, q.spread_pct)
47
+ ```
48
+
49
+ !!! warning "Quotes that omit output are marked a floor"
50
+ Output costs more than input on every model on the sheet. If a job carries
51
+ no output-token signal, `quote()` prices its output at **zero** and labels
52
+ the whole quote a `FLOOR` — a stated floor is safer than an invented
53
+ number. Give it `max_tokens`, or explicit counts:
54
+
55
+ ```python
56
+ offpeak.job("claude-haiku-4-5", prompt, max_tokens=512)
57
+ offpeak.Job(model=..., messages=[...],
58
+ metadata={"input_tokens": 800, "output_tokens": 200})
59
+ ```
60
+
61
+ `Quote.basis` reports the provenance of every figure.
62
+
63
+ !!! warning "Reasoning models spend the ceiling before they speak"
64
+ On models that reason before answering — OpenAI's gpt-5 family, the
65
+ o-series — `max_tokens` caps **reasoning plus visible output**, and the
66
+ reasoning goes first. Set it too low and the job bills a full ceiling of
67
+ reasoning tokens and returns an empty string: a `Result` that is
68
+ technically ok, costs real money, and says nothing.
69
+
70
+ This is not hypothetical. A real batch here ran 24 jobs at
71
+ `max_tokens=16`, billed 374 output tokens, and returned 24 empty strings.
72
+ Give a reasoning model room — hundreds of tokens, not dozens — and price
73
+ the ceiling you actually set, which is what `quote()` does.
74
+
75
+ `offpeak` sends the ceiling under whichever name the venue wants
76
+ (`max_completion_tokens` where the model demands it), but it cannot make a
77
+ ceiling large enough to answer in.
78
+
79
+ ### If you know roughly what it will write
80
+
81
+ A floor is honest but not always useful. When you do have a sense of the output
82
+ size, say so — and the quote prices it, marked `EST` rather than `FLOOR`:
83
+
84
+ ```python
85
+ # Across the run: assume each job writes a quarter of what it reads.
86
+ offpeak.quote(jobs, deadline="06:00", assumed_output_ratio=0.25)
87
+
88
+ # Or per job, which wins over a ratio and over max_tokens:
89
+ offpeak.Job(model=..., messages=[...],
90
+ metadata={"expected_output_tokens": 300})
91
+ ```
92
+
93
+ ```
94
+ EST 5000 job(s) priced on an assumed output size, not a measured one
95
+ the assumption is yours; the bill moves with what the model actually writes
96
+ ```
97
+
98
+ Both are opt-in. Without one, nothing is assumed on your behalf: the default
99
+ stays the floor. The two marks mean different things and a quote can carry both
100
+ — `FLOOR` is understated by construction, `EST` can land either side of the
101
+ bill. `Quote.is_floor` and `Quote.is_estimated` are the same distinction in
102
+ code, and a ratio applies only to jobs with no signal of their own, so explicit
103
+ counts and `max_tokens` are never overridden by it.
104
+
105
+ ## 2. Run — against a deadline
106
+
107
+ ```python
108
+ results = offpeak.run(jobs, deadline="06:00")
109
+ ```
110
+
111
+ Each job goes to the batch tier of a venue that supports its model. `offpeak`
112
+ polls until the work lands. If the batch has not completed by the time the
113
+ remaining window shrinks to the risk buffer, it cancels and re-runs the
114
+ stragglers synchronously at list price — you stated a deadline, and it is met.
115
+
116
+ Deadlines accept `"06:00"` (next occurrence), `"6h"`, `"90m"`, a `datetime`, a
117
+ `timedelta`, seconds, or an ISO 8601 string.
118
+
119
+ ## 3. Read the receipt
120
+
121
+ ```python
122
+ print(offpeak.receipt(results))
123
+ ```
124
+
125
+ ```
126
+ OFFPEAK SETTLEMENT ────────────────────────────
127
+ jobs 5000 (5000 ok, 120 sync fallback, 0 failed)
128
+ sla 5000/5000 met
129
+ venues anthropic:batch 3000 · openai:batch 2000
130
+ tokens 41,000,000 in · 3,200,000 out
131
+ list $2,469.00
132
+ paid $1,234.50
133
+ captured $1,234.50 (50.0%)
134
+ left $29.63 on the table (120 job(s) missed the batch tier)
135
+ prices snapshot 2026-08-21 — override via offpeak.prices
136
+ ───────────────────────────────────────────────
137
+ ```
138
+
139
+ `left on the table` is what the sync fallback gave up by missing the batch
140
+ tier. Per-job receipts render the same way — `print(results[0].receipt)` — with
141
+ sub-cent precision, so a small run reports what it cost rather than `$0.00`.
142
+
143
+ ## Prices
144
+
145
+ The bundled sheet is a dated snapshot. Providers move prices; override at
146
+ runtime rather than waiting for a release:
147
+
148
+ ```python
149
+ offpeak.prices.register_price("my-fine-tune", input_per_m=4.0, output_per_m=16.0)
150
+ ```
151
+
152
+ Unknown models settle as `None`, never a guess.
@@ -0,0 +1,48 @@
1
+ # API reference
2
+
3
+ Everything below is exported from the package root: `offpeak.run`,
4
+ `offpeak.Job`, and so on.
5
+
6
+ ## Running work
7
+
8
+ ::: offpeak.run
9
+
10
+ ::: offpeak.quote
11
+
12
+ ::: offpeak.receipt
13
+
14
+ ::: offpeak.job
15
+
16
+ ## Types
17
+
18
+ ::: offpeak.Job
19
+
20
+ ::: offpeak.Result
21
+
22
+ ::: offpeak.Receipt
23
+
24
+ ::: offpeak.Status
25
+
26
+ ::: offpeak.Settlement
27
+
28
+ ::: offpeak.Quote
29
+
30
+ ::: offpeak.VenueQuote
31
+
32
+ ## Deadlines
33
+
34
+ ::: offpeak.parse_deadline
35
+
36
+ ::: offpeak.seconds_until
37
+
38
+ ## Venues
39
+
40
+ ::: offpeak.Venue
41
+
42
+ ::: offpeak.BatchState
43
+
44
+ ::: offpeak.default_venues
45
+
46
+ ## Prices
47
+
48
+ ::: offpeak.prices