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.
@@ -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,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Other
13
+ .env
14
+ sample_out*
@@ -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.
@@ -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.
@@ -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
+ [![CI](https://github.com/takumi0shibata/tokenrail/actions/workflows/ci.yml/badge.svg)](https://github.com/takumi0shibata/tokenrail/actions/workflows/ci.yml)
32
+ [![PyPI](https://img.shields.io/pypi/v/tokenrail)](https://pypi.org/project/tokenrail/)
33
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/tokenrail/)
34
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](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)