tokenrail 0.4.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.
- tokenrail-0.4.0/.github/workflows/ci.yml +37 -0
- tokenrail-0.4.0/.github/workflows/release.yml +43 -0
- tokenrail-0.4.0/.gitignore +14 -0
- tokenrail-0.4.0/.python-version +1 -0
- tokenrail-0.4.0/CHANGELOG.md +138 -0
- tokenrail-0.4.0/LICENSE +21 -0
- tokenrail-0.4.0/PKG-INFO +338 -0
- tokenrail-0.4.0/README.md +310 -0
- tokenrail-0.4.0/pyproject.toml +67 -0
- tokenrail-0.4.0/src/tokenrail/__init__.py +32 -0
- tokenrail-0.4.0/src/tokenrail/catalog.py +320 -0
- tokenrail-0.4.0/src/tokenrail/client.py +58 -0
- tokenrail-0.4.0/src/tokenrail/executor.py +249 -0
- tokenrail-0.4.0/src/tokenrail/monitor.py +457 -0
- tokenrail-0.4.0/src/tokenrail/providers/__init__.py +3 -0
- tokenrail-0.4.0/src/tokenrail/providers/base.py +23 -0
- tokenrail-0.4.0/src/tokenrail/providers/openai.py +452 -0
- tokenrail-0.4.0/src/tokenrail/py.typed +0 -0
- tokenrail-0.4.0/src/tokenrail/sinks.py +106 -0
- tokenrail-0.4.0/src/tokenrail/types.py +258 -0
- tokenrail-0.4.0/tests/test_monitor_and_executor.py +558 -0
- tokenrail-0.4.0/tests/test_openai_provider.py +572 -0
- tokenrail-0.4.0/tests/test_prompt_cache.py +176 -0
- tokenrail-0.4.0/uv.lock +569 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Run tests
|
|
21
|
+
run: uv run --python ${{ matrix.python-version }} pytest -q
|
|
22
|
+
|
|
23
|
+
lint:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v5
|
|
28
|
+
- name: Ruff check
|
|
29
|
+
run: uv run ruff check src tests
|
|
30
|
+
|
|
31
|
+
build:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
- uses: astral-sh/setup-uv@v5
|
|
36
|
+
- name: Build sdist and wheel
|
|
37
|
+
run: uv build
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v5
|
|
13
|
+
- name: Check tag matches package version
|
|
14
|
+
run: |
|
|
15
|
+
PKG_VERSION=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)
|
|
16
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
17
|
+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
18
|
+
echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $PKG_VERSION" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
- name: Run tests
|
|
22
|
+
run: uv run pytest -q
|
|
23
|
+
- name: Build sdist and wheel
|
|
24
|
+
run: uv build
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment:
|
|
34
|
+
name: pypi
|
|
35
|
+
url: https://pypi.org/p/tokenrail
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
for its 0.x development releases, with a one-time version reset in 0.4.0.
|
|
8
|
+
Earlier 1.x and 2.x entries are retained as published history; their stable
|
|
9
|
+
designation was premature and does not describe the project's current status.
|
|
10
|
+
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
## [0.4.0] - 2026-10-08
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- GPT-6 Astra, GPT-6.1 Sol, GPT-6 Sol, and GPT-6 Luna catalog entries with
|
|
18
|
+
standard-tier base prices and cache-write rates.
|
|
19
|
+
- GPT-6 reasoning-effort and sampling-parameter validation for both
|
|
20
|
+
`responses.create(...)` and `responses.parse(...)`.
|
|
21
|
+
- OpenAI prompt-cache settings are forwarded unchanged for both
|
|
22
|
+
`responses.create(...)` and `responses.parse(...)`.
|
|
23
|
+
- Conflicting request fields in `extra_body` are rejected before sending.
|
|
24
|
+
- `cache_write_tokens` usage metrics on responses and batch/model statistics.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Reset the package version from 2.0.0 to 0.4.0 and the development status from
|
|
29
|
+
Production/Stable to Beta. This continues development from the existing code;
|
|
30
|
+
it does not restore an older implementation. Public API compatibility is not
|
|
31
|
+
yet guaranteed between 0.x minor releases.
|
|
32
|
+
- GPT-5.6 Sol and the `gpt-5.6` alias use OpenAI's current promotional prices:
|
|
33
|
+
$4.00 input, $0.40 cached input, $5.00 cache writes, and $20.00 output per
|
|
34
|
+
million tokens, verified on October 8, 2026.
|
|
35
|
+
- GPT-5.6 cache writes are included in cost estimates at 1.25 times the normal
|
|
36
|
+
input rate.
|
|
37
|
+
- GPT-5.6 Terra and Luna prices now match OpenAI's August 2026 price cuts,
|
|
38
|
+
including cached-input and 1.25-times cache-write rates.
|
|
39
|
+
- Prompt caching uses only OpenAI's standard request settings; tokenrail does
|
|
40
|
+
not plan prefixes, generate or shard keys, prewarm caches, or impose per-key
|
|
41
|
+
submit limits. `BatchExecutor.prompt_cache` and `PromptCacheConfig` are removed.
|
|
42
|
+
- Progress output reports cache reads and writes independently of request settings.
|
|
43
|
+
- Progress output preserves unknown payer strings instead of displaying `?`,
|
|
44
|
+
and falls back to `billing.payer` when the cost breakdown has no payer.
|
|
45
|
+
|
|
46
|
+
## [2.0.0] - 2026-08-03
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- GPT-5.6 Sol, Terra, and Luna capability and base-price catalog entries,
|
|
51
|
+
including the `gpt-5.6` alias for Sol.
|
|
52
|
+
- `ModelCatalogFallbackWarning` identifies unregistered models and the catalog
|
|
53
|
+
entries used as capability or pricing fallbacks.
|
|
54
|
+
- Payer state, payer switch count, and raw payer request counts on
|
|
55
|
+
`StatsSnapshot`.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- `RollingMetricsMonitor` now prints a compact request line, explicit payer
|
|
60
|
+
transitions, periodic summaries, and a final batch summary by default.
|
|
61
|
+
Pass `verbose=True` to retain the previous progress format.
|
|
62
|
+
- Monitor output supports automatic or forced ANSI emphasis without an added
|
|
63
|
+
dependency.
|
|
64
|
+
- GPT-5.6 cost estimates use current base standard-tier prices. Long-context,
|
|
65
|
+
cache-write, Batch/Fast/Flex, and regional price adjustments remain outside
|
|
66
|
+
the estimator.
|
|
67
|
+
|
|
68
|
+
## [1.1.0] - 2026-06-14
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- Structured output parsing for batch jobs. `BatchExecutor` now calls
|
|
73
|
+
`responses.parse(...)` automatically for items that include `text_format`.
|
|
74
|
+
- `client.responses.parse(...)` and `OpenAIProvider.parse(...)`.
|
|
75
|
+
- `NormalizedResponse.output_parsed` and `NormalizedResponse.refusal`.
|
|
76
|
+
- Default JSONL result records now include `output_parsed` and `refusal`.
|
|
77
|
+
- README example for Pydantic structured output batches.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- `response_format` remains the low-level JSON Schema path for
|
|
82
|
+
`responses.create(...)`; `text_format` is the high-level Pydantic parsing path.
|
|
83
|
+
They are rejected when used together.
|
|
84
|
+
|
|
85
|
+
## [1.0.0] - 2026-06-11
|
|
86
|
+
|
|
87
|
+
First stable release. The public API surface is now covered by semantic
|
|
88
|
+
versioning: `RailClient`, `BatchExecutor`, `batch_items_from_queries`,
|
|
89
|
+
`RollingMetricsMonitor`, `ResultsJsonlSink`, `PerRequestJsonSink`,
|
|
90
|
+
`OpenAIProvider`, and the types exported from `tokenrail`.
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- `py.typed` marker — the package now ships inline type information (PEP 561).
|
|
95
|
+
- `tokenrail.__version__`.
|
|
96
|
+
- Docstrings across the public API.
|
|
97
|
+
- Complete packaging metadata (license, classifiers, project URLs).
|
|
98
|
+
- CI test workflow across Python 3.10–3.14.
|
|
99
|
+
- First release published to [PyPI](https://pypi.org/project/tokenrail/);
|
|
100
|
+
releases are published automatically from `v*` tags via PyPI Trusted
|
|
101
|
+
Publishing.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- Minimum supported Python lowered from 3.11 to 3.10.
|
|
106
|
+
- `catalog.get_model_pricing` no longer carries a dead `service_tier` branch;
|
|
107
|
+
non-default tiers explicitly fall back to default-tier pricing.
|
|
108
|
+
|
|
109
|
+
## [0.2.1] - 2026-06-10
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
|
|
113
|
+
- Client-side RPM and TPM submit throttling in `BatchExecutor`
|
|
114
|
+
(`max_rpm` / `max_tpm`).
|
|
115
|
+
|
|
116
|
+
### Removed
|
|
117
|
+
|
|
118
|
+
- vLLM provider support; the library is OpenAI-only.
|
|
119
|
+
|
|
120
|
+
## [0.1.3] - 2026-05-20
|
|
121
|
+
|
|
122
|
+
### Changed
|
|
123
|
+
|
|
124
|
+
- Removed the custom OpenAI retry loop in favor of the SDK's built-in
|
|
125
|
+
`max_retries`.
|
|
126
|
+
|
|
127
|
+
## [0.1.2] - 2026-05-15
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- ETA progress reporting in `RollingMetricsMonitor`.
|
|
132
|
+
|
|
133
|
+
## [0.1.0] - 2026-05-13
|
|
134
|
+
|
|
135
|
+
### Added
|
|
136
|
+
|
|
137
|
+
- Initial release: `RailClient`, `BatchExecutor`, rolling metrics monitor,
|
|
138
|
+
JSONL and per-request sinks, model capability/pricing catalog.
|
tokenrail-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Takumi Shibata
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tokenrail-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tokenrail
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Thin client and batch execution helpers for OpenAI Responses API workloads.
|
|
5
|
+
Project-URL: Homepage, https://github.com/takumi0shibata/tokenrail
|
|
6
|
+
Project-URL: Repository, https://github.com/takumi0shibata/tokenrail
|
|
7
|
+
Project-URL: Issues, https://github.com/takumi0shibata/tokenrail/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/takumi0shibata/tokenrail/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Takumi Shibata
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: batch,cost-tracking,llm,openai,rate-limit,responses-api,token
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: openai>=1.108.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# tokenrail
|
|
30
|
+
|
|
31
|
+
[](https://github.com/takumi0shibata/tokenrail/actions/workflows/ci.yml)
|
|
32
|
+
[](https://pypi.org/project/tokenrail/)
|
|
33
|
+
[](https://pypi.org/project/tokenrail/)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
|
|
36
|
+
`tokenrail` is a small Python library for running OpenAI Responses API jobs with a `client.responses`-style surface.
|
|
37
|
+
|
|
38
|
+
It focuses on:
|
|
39
|
+
|
|
40
|
+
- thread-based OpenAI batch execution
|
|
41
|
+
- structured output parsing for Pydantic models
|
|
42
|
+
- client-side RPM / TPM submit throttling
|
|
43
|
+
- OpenAI prompt-cache settings passed through unchanged, with read/write usage tracking
|
|
44
|
+
- per-model token / cost monitoring with ETA progress reporting
|
|
45
|
+
- resumable JSONL and per-request result writing
|
|
46
|
+
|
|
47
|
+
Fully typed (PEP 561), supports Python 3.10+.
|
|
48
|
+
|
|
49
|
+
The library is in beta. The public API is still evolving, and breaking changes
|
|
50
|
+
may occur between 0.x minor versions.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add tokenrail
|
|
56
|
+
# or
|
|
57
|
+
pip install tokenrail
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
To track an unreleased revision instead, depend on the Git repository directly:
|
|
61
|
+
|
|
62
|
+
```toml
|
|
63
|
+
[tool.uv.sources]
|
|
64
|
+
tokenrail = { git = "https://github.com/takumi0shibata/tokenrail", branch = "main" }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Set your OpenAI API key in the consuming project before use:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export OPENAI_API_KEY=...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from tokenrail import BatchExecutor, ResultsJsonlSink, PerRequestJsonSink, RailClient, RollingMetricsMonitor
|
|
77
|
+
from tokenrail.executor import batch_items_from_queries
|
|
78
|
+
|
|
79
|
+
client = RailClient.openai(max_retries=6)
|
|
80
|
+
|
|
81
|
+
queries = {
|
|
82
|
+
"1": [{"role": "user", "content": "Summarize this paper in 3 bullets."}],
|
|
83
|
+
"2": [{"role": "user", "content": "Extract the key assumptions."}],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
items = batch_items_from_queries(
|
|
87
|
+
queries,
|
|
88
|
+
model="gpt-5.4-mini-2026-03-17",
|
|
89
|
+
reasoning_effort="medium",
|
|
90
|
+
verbosity="low",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Consolidate only the necessary elements from all processing results into a single file.
|
|
94
|
+
result_sink = ResultsJsonlSink(
|
|
95
|
+
"out/results.jsonl",
|
|
96
|
+
projector=lambda response: {
|
|
97
|
+
"id": response.id,
|
|
98
|
+
"text": response.output_text,
|
|
99
|
+
"model": response.model,
|
|
100
|
+
"usage": response.usage.to_dict(),
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
# Save the raw output of each query.
|
|
104
|
+
per_request_sink = PerRequestJsonSink("out/")
|
|
105
|
+
|
|
106
|
+
executor = BatchExecutor(
|
|
107
|
+
client=client,
|
|
108
|
+
max_workers=16,
|
|
109
|
+
max_rpm=500,
|
|
110
|
+
max_tpm=200_000,
|
|
111
|
+
sinks=[result_sink, per_request_sink],
|
|
112
|
+
monitor=RollingMetricsMonitor(),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
stats = executor.run(items)
|
|
116
|
+
print(stats.to_dict())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Prompt caching
|
|
120
|
+
|
|
121
|
+
tokenrail forwards OpenAI's prompt-cache settings and input content as supplied.
|
|
122
|
+
It does not detect shared prefixes, insert breakpoints, generate or split keys,
|
|
123
|
+
prewarm caches, or impose cache-specific submit limits. These settings work with
|
|
124
|
+
both `responses.create(...)` and `responses.parse(...)`, including batch items.
|
|
125
|
+
|
|
126
|
+
| Setting | Purpose |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `prompt_cache_options` | Cache mode, TTL, prewarming, and diagnostics where supported |
|
|
129
|
+
| `prompt_cache_breakpoint` inside `input` content | The end of a reusable prefix |
|
|
130
|
+
| `prompt_cache_key` | A caller-supplied key, preserved exactly |
|
|
131
|
+
| `prompt_cache_retention` | Retention for older models that support it |
|
|
132
|
+
|
|
133
|
+
Omitting these settings keeps the OpenAI API defaults, including implicit
|
|
134
|
+
caching. On GPT-5.6 and later, writing an eligible prefix costs 1.25 times the
|
|
135
|
+
ordinary input rate, so a prefix that is never reused can cost more than an
|
|
136
|
+
uncached input. Setting support and cache eligibility depend on the model.
|
|
137
|
+
|
|
138
|
+
Use the API defaults:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from tokenrail import RailClient
|
|
142
|
+
|
|
143
|
+
client = RailClient.openai()
|
|
144
|
+
response = client.responses.create(model="gpt-6.1-sol", input="Summarize this paper.")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Cache only a reusable prefix on GPT-5.6 and later. Put the shared content in its
|
|
148
|
+
own block and mark its end; content after the breakpoint stays outside the cache
|
|
149
|
+
write. The prefix must render to at least 1,024 visible input tokens. Top-level
|
|
150
|
+
`instructions` cannot contain a breakpoint, so supply reusable instructions in
|
|
151
|
+
a developer content block:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from tokenrail import BatchExecutor, batch_items_from_queries
|
|
155
|
+
|
|
156
|
+
shared_block = {
|
|
157
|
+
"type": "input_text",
|
|
158
|
+
"text": "Shared rubric, examples, and reference material ...", # At least 1,024 tokens.
|
|
159
|
+
"prompt_cache_breakpoint": {"mode": "explicit"},
|
|
160
|
+
}
|
|
161
|
+
items = batch_items_from_queries(
|
|
162
|
+
{
|
|
163
|
+
item_id: [
|
|
164
|
+
{"role": "developer", "content": [shared_block]},
|
|
165
|
+
{"role": "user", "content": question},
|
|
166
|
+
]
|
|
167
|
+
for item_id, question in {
|
|
168
|
+
"paper-1": "Summarize paper one.",
|
|
169
|
+
"paper-2": "Summarize paper two.",
|
|
170
|
+
}.items()
|
|
171
|
+
},
|
|
172
|
+
model="gpt-6.1-sol",
|
|
173
|
+
prompt_cache_options={"mode": "explicit"},
|
|
174
|
+
)
|
|
175
|
+
stats = BatchExecutor(client=client, max_workers=16, max_rpm=120).run(items)
|
|
176
|
+
print(stats.cached_tokens) # Tokens read from cache.
|
|
177
|
+
print(stats.cache_write_tokens) # Tokens newly written to cache.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Avoid prompt-cache reads and writes on GPT-5.6 and later by using explicit mode
|
|
181
|
+
with no breakpoints:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
response = client.responses.create(
|
|
185
|
+
model="gpt-6.1-sol",
|
|
186
|
+
input="A one-off prompt ...",
|
|
187
|
+
prompt_cache_options={"mode": "explicit"},
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Choose keys, breakpoints, and reuse according to your workload. When supplying a
|
|
192
|
+
key, keep it stable across related requests. tokenrail records cache read/write
|
|
193
|
+
tokens and estimates their costs; it does not guarantee a hit or a saving.
|
|
194
|
+
Conflicting values supplied in normal request arguments and `extra_body` raise
|
|
195
|
+
`ValueError` before sending. `store=False` controls response storage, not prompt
|
|
196
|
+
caching. See the [OpenAI prompt caching guide](https://developers.openai.com/api/docs/guides/prompt-caching)
|
|
197
|
+
for current model support, pricing, and cache lifetime.
|
|
198
|
+
|
|
199
|
+
## Structured output batches
|
|
200
|
+
|
|
201
|
+
Pass a Pydantic model as `text_format` when building batch items. `BatchExecutor`
|
|
202
|
+
will call `responses.parse(...)` for those items and store the validated object
|
|
203
|
+
on `response.output_parsed`.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from pydantic import BaseModel
|
|
207
|
+
|
|
208
|
+
from tokenrail import BatchExecutor, RailClient, ResultsJsonlSink
|
|
209
|
+
from tokenrail.executor import batch_items_from_queries
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class PaperSummary(BaseModel):
|
|
213
|
+
title: str
|
|
214
|
+
key_assumptions: list[str]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
client = RailClient.openai(max_retries=6)
|
|
218
|
+
|
|
219
|
+
items = batch_items_from_queries(
|
|
220
|
+
{
|
|
221
|
+
"paper-1": [{"role": "user", "content": "Extract the title and assumptions from this paper: ..."}],
|
|
222
|
+
"paper-2": [{"role": "user", "content": "Extract the title and assumptions from this paper: ..."}],
|
|
223
|
+
},
|
|
224
|
+
model="gpt-5.4-mini-2026-03-17",
|
|
225
|
+
reasoning_effort="medium",
|
|
226
|
+
text_format=PaperSummary,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
sink = ResultsJsonlSink(
|
|
230
|
+
"out/structured-results.jsonl",
|
|
231
|
+
projector=lambda response: {
|
|
232
|
+
"id": response.id,
|
|
233
|
+
"summary": response.output_parsed.model_dump(mode="json") if response.output_parsed else None,
|
|
234
|
+
"refusal": response.refusal,
|
|
235
|
+
"usage": response.usage.to_dict(),
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
stats = BatchExecutor(client=client, sinks=[sink], max_workers=16).run(items)
|
|
240
|
+
print(stats.to_dict())
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Use `response_format={...}` with `client.responses.create(...)` when you want to
|
|
244
|
+
provide a raw JSON Schema yourself. Use `text_format=YourModel` for Pydantic
|
|
245
|
+
parsing; `response_format` and `text_format` cannot be used together. Do not set
|
|
246
|
+
`verbosity` on structured output batches that use `text_format`, because the
|
|
247
|
+
OpenAI SDK's `responses.parse(...)` path does not accept that combination.
|
|
248
|
+
|
|
249
|
+
## Configuration notes
|
|
250
|
+
|
|
251
|
+
- `max_retries` configures the OpenAI Python SDK client's built-in retry behavior. `tokenrail` does not add its own retry loop on top.
|
|
252
|
+
- `max_rpm` and `max_tpm` are optional client-side submit limits. When a limit is set, `BatchExecutor` waits before submitting more work instead of raising its effective concurrency above the configured rate.
|
|
253
|
+
- Request failures are captured as error records (written to sinks and counted in stats) rather than raised, so one failing item does not abort the batch.
|
|
254
|
+
- `base_url` is passed through to the OpenAI Python SDK for callers that need an SDK-level custom endpoint.
|
|
255
|
+
|
|
256
|
+
## Resume behavior
|
|
257
|
+
|
|
258
|
+
`BatchExecutor` reads completed ids from the first configured sink before it starts. Re-running the same job with the same output path skips records that are already present, then writes only the remaining requests.
|
|
259
|
+
|
|
260
|
+
If you use a custom `projector` with `ResultsJsonlSink`, make sure it keeps an `"id"` field — resume relies on it.
|
|
261
|
+
|
|
262
|
+
## Progress output
|
|
263
|
+
|
|
264
|
+
`RollingMetricsMonitor` keeps request-specific details short and prints batch
|
|
265
|
+
metrics separately:
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
tokenrail · 100 requests
|
|
269
|
+
PAYER: openai — costs are covered
|
|
270
|
+
0001 ok req-0001 model=gpt-5.6-terra 1.3k tok (40% cached / 0% cache-write) $0.002000 oai 1.4s
|
|
271
|
+
── 50/100 · 50% · 00:00:14 · ETA 00:00:14 · 58 rpm · 74k tpm · $0.100 (oai 100% / dev $0.000) · cache r40%/w2%
|
|
272
|
+
!! PAYER SWITCH: openai → developer at 53/100 (00:00:15) — now billed to you
|
|
273
|
+
0053 ok req-0053 1.2k tok (38% cached / 0% cache-write) $0.001900 DEV 1.1s
|
|
274
|
+
Done 100/100 · 99 ok / 1 errors · 00:00:29
|
|
275
|
+
Total $0.198 — openai $0.104 (53%) / developer $0.094 (47%)
|
|
276
|
+
Prompt cache: 48k read / 2.4k written
|
|
277
|
+
Payer switches: 1
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Periodic summaries are emitted every 50 requests or 30 seconds by default.
|
|
281
|
+
Configure them with `summary_every` and `summary_interval`, adjust payer
|
|
282
|
+
transition hysteresis with `payer_switch_threshold`, and control ANSI styling
|
|
283
|
+
with `color`. Pass `verbose=True` to use the legacy `[n/total] id=...` format.
|
|
284
|
+
`printer=None` still disables all monitor output.
|
|
285
|
+
|
|
286
|
+
Request lines show `oai` for `openai`, `DEV` for `developer`, and the original
|
|
287
|
+
string for any other payer value. A missing or empty payer is shown as `?`.
|
|
288
|
+
The monitor reads the payer from `cost`, falling back to `billing.payer` when
|
|
289
|
+
unavailable. Unknown payer values count toward `unknown_payer_requests` but
|
|
290
|
+
do not drive payer transitions.
|
|
291
|
+
|
|
292
|
+
## Cost tracking
|
|
293
|
+
|
|
294
|
+
Standard-tier base prices in USD per million tokens, checked against the
|
|
295
|
+
[official OpenAI pricing page](https://developers.openai.com/api/docs/pricing)
|
|
296
|
+
on October 8, 2026:
|
|
297
|
+
|
|
298
|
+
| Model | Input | Cached input | Cache writes | Output |
|
|
299
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
300
|
+
| `gpt-6-astra` | $10.00 | $1.00 | $12.50 | $50.00 |
|
|
301
|
+
| `gpt-6.1-sol` | $2.00 | $0.10 | $2.50 | $10.00 |
|
|
302
|
+
| `gpt-6-sol` | $2.00 | $0.20 | $2.50 | $10.00 |
|
|
303
|
+
| `gpt-6-luna` | $0.10 | $0.01 | $0.125 | $0.50 |
|
|
304
|
+
| `gpt-5.6-sol` / `gpt-5.6` | $4.00 | $0.40 | $5.00 | $20.00 |
|
|
305
|
+
| `gpt-5.6-terra` | $2.00 | $0.20 | $2.50 | $12.00 |
|
|
306
|
+
| `gpt-5.6-luna` | $0.20 | $0.02 | $0.25 | $1.20 |
|
|
307
|
+
|
|
308
|
+
GPT-5.6 Sol uses the current promotional rate, available at least through
|
|
309
|
+
November 21, 2026 according to OpenAI.
|
|
310
|
+
|
|
311
|
+
- GPT-5.6 and GPT-6 cache writes are estimated at 1.25 times each model's ordinary input rate and are tracked separately from cache reads.
|
|
312
|
+
- Costs use the checked-in base, standard-tier pricing table (`tokenrail.catalog`). The estimate does not apply GPT-5.6/GPT-6 long-context rates above 272K input tokens, Batch/Fast/Flex/Ultrafast pricing, or regional uplifts. The official pricing page is authoritative.
|
|
313
|
+
- Models without a pricing entry get `cost=None`. If an unregistered model partially matches an older catalog entry, tokenrail emits `ModelCatalogFallbackWarning` once per model and names the capability and pricing entries used as fallbacks.
|
|
314
|
+
- OpenAI cost allocation is inferred from `billing.payer` in the response body. When `payer == "openai"`, the nominal request cost is counted as OpenAI-covered rather than developer-billed.
|
|
315
|
+
- `reasoning_effort` is gated to supported `gpt-5`, `gpt-6`, and `o`-series models in the checked-in capability registry.
|
|
316
|
+
- GPT-6 Astra and GPT-6.1 Sol support `low`, `medium`, `high`, `xhigh`, and `max` reasoning efforts; `temperature` and `top_p` are rejected before sending a request. GPT-6 Sol and Luna additionally support `none`, which must be explicitly selected to use `temperature` or `top_p`. These checks apply to both `responses.create(...)` and `responses.parse(...)`. See the [GPT-6 guide](https://developers.openai.com/api/docs/guides/latest-model).
|
|
317
|
+
|
|
318
|
+
Fallback warnings can be filtered with Python's standard warning controls:
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
import warnings
|
|
322
|
+
|
|
323
|
+
from tokenrail.catalog import ModelCatalogFallbackWarning
|
|
324
|
+
|
|
325
|
+
warnings.filterwarnings("ignore", category=ModelCatalogFallbackWarning)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Development
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
uv sync
|
|
332
|
+
uv run pytest
|
|
333
|
+
uv run ruff check src tests
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
[MIT](LICENSE)
|