decision-toolkit 0.1.6__tar.gz → 0.2.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.
Files changed (90) hide show
  1. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/CHANGELOG.md +49 -2
  2. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/PKG-INFO +16 -10
  3. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/README.md +15 -9
  4. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/CORE.md +1 -1
  5. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/EXAMPLES.md +4 -0
  6. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/RELEASE.md +12 -8
  7. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/REST.md +102 -62
  8. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/VARIANTS.md +28 -0
  9. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/index.md +1 -1
  10. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/pyproject.toml +1 -1
  11. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/__init__.py +1 -1
  12. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/contracts/__init__.py +2 -2
  13. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/contracts/mapping.py +36 -12
  14. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/contracts/openai_decisions_beta.py +4 -8
  15. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/contracts/systemone_v1.py +43 -27
  16. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/decision_model.py +14 -1
  17. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/registry.py +10 -5
  18. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/schema.py +1 -1
  19. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/server.py +76 -12
  20. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_cli.py +21 -0
  21. decision_toolkit-0.2.0/tests/test_contracts_conformance.py +109 -0
  22. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_decision_model.py +21 -0
  23. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_fuzz_rest.py +7 -3
  24. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_operations.py +1 -1
  25. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_schema.py +1 -2
  26. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_server.py +246 -23
  27. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/uv.lock +1 -1
  28. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/.github/workflows/ci.yml +0 -0
  29. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/.github/workflows/docs.yml +0 -0
  30. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/.github/workflows/release.yml +0 -0
  31. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/.gitignore +0 -0
  32. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/CONTRIBUTING.md +0 -0
  33. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/LICENSE +0 -0
  34. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/SECURITY.md +0 -0
  35. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/THIRD_PARTY_NOTICES.md +0 -0
  36. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/README.md +0 -0
  37. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/__init__.py +0 -0
  38. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/load.py +0 -0
  39. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/results/2026-10-20-calibration.json +0 -0
  40. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/results/2026-10-20-load.json +0 -0
  41. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/results/2026-10-20-smoke.json +0 -0
  42. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/run.py +0 -0
  43. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/suites/calibration.jsonl +0 -0
  44. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/bench/suites/smoke.jsonl +0 -0
  45. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/BENCH.md +0 -0
  46. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/CLI.md +0 -0
  47. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/OPERATIONS.md +0 -0
  48. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/ROADMAP.md +0 -0
  49. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/models/README.md +0 -0
  50. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/models/gliner2.5-decide.md +0 -0
  51. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/models/gliner2.5-multi.md +0 -0
  52. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/models/julia-1.md +0 -0
  53. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/models/laya.md +0 -0
  54. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/docs/models/opendecider-nano.md +0 -0
  55. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/__main__.py +0 -0
  56. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/adapters/__init__.py +0 -0
  57. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/adapters/base.py +0 -0
  58. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/adapters/gliner2.py +0 -0
  59. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/adapters/gliner2_decide.py +0 -0
  60. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/adapters/julia.py +0 -0
  61. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/adapters/laya.py +0 -0
  62. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/adapters/opendecider.py +0 -0
  63. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/calibration.py +0 -0
  64. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/catalog.py +0 -0
  65. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/cli.py +0 -0
  66. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/config.py +0 -0
  67. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/contracts/common.py +0 -0
  68. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/engine/__init__.py +0 -0
  69. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/errors.py +0 -0
  70. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/limits.py +0 -0
  71. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/metrics.py +0 -0
  72. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/models.py +0 -0
  73. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/observability.py +0 -0
  74. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/py.typed +0 -0
  75. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/results.py +0 -0
  76. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/src/decision_toolkit/tracing.py +0 -0
  77. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/__init__.py +0 -0
  78. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/conftest.py +0 -0
  79. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_adapters_gliner2.py +0 -0
  80. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_adapters_gliner2_decide.py +0 -0
  81. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_adapters_julia.py +0 -0
  82. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_adapters_laya.py +0 -0
  83. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_adapters_opendecider.py +0 -0
  84. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_bench.py +0 -0
  85. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_catalog.py +0 -0
  86. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_config.py +0 -0
  87. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_core_phase1.py +0 -0
  88. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_integration_gliner2.py +0 -0
  89. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_models.py +0 -0
  90. {decision_toolkit-0.1.6 → decision_toolkit-0.2.0}/tests/test_results.py +0 -0
@@ -1,8 +1,53 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project are documented here. Version `0.1.6` is
3
+ All notable changes to this project are documented here. Version `0.2.0` is
4
4
  published on PyPI.
5
5
 
6
+ ## [0.2.0] — 2026-10-20
7
+
8
+ REST conformance correction. The previous documentation called the TypeSafe
9
+ contract "System One 1.14"; that version was not real. The public TypeSafe API
10
+ schema is `0.2.0`. This release aligns the request/response shapes and error
11
+ behavior with the published schemas, documents unsupported vision/assets as an
12
+ explicit text-only limitation, and removes invented OpenAI response fields.
13
+
14
+ ### Fixed
15
+
16
+ - System One is now pinned to the published TypeSafe API schema `0.2.0`, not
17
+ the invented `1.14` label. `/v1/info` reports `0.2.0`.
18
+ - System One accepts rich instructions/criteria, the standard `assets` field,
19
+ one-level scores and non-negative `output_tokens`; schema validation returns
20
+ `422` with a FastAPI-style `detail` array. Assets/images return `501` because
21
+ the catalog is text-only.
22
+ - OpenAI Decisions responses now expose the public `model`, `answers` and
23
+ `usage` fields (including usage details), without invented `id`, `object`,
24
+ `created_at`, `status` or `safety_identifier` fields. Toolkit metadata remains
25
+ namespaced under `x_decision_toolkit`.
26
+ - `variant` is explicitly documented as a local extension, not a TypeSafe or
27
+ OpenAI field. Standard clients select a model and omit it.
28
+
29
+ ## [0.1.7] — 2026-10-20
30
+
31
+ Variant consistency release. The selected quantization now agrees across the
32
+ library, CLI and REST API, including catalog defaults, persisted `dtk models
33
+ use` selections, per-call overrides, readiness/info metadata and response
34
+ payloads.
35
+
36
+ ### Fixed
37
+
38
+ - `DecisionModel.variant` reports the explicit, persisted or catalog-default
39
+ variant before and after resolution, without inheriting another model's
40
+ selection.
41
+ - The REST registry resolves the catalog default for a configured model and
42
+ honors the persisted variant for the current model. `/v1/info`, `/ready`,
43
+ `/v1/models` and `/v1/decision-models` expose the effective default and
44
+ `current_variant`.
45
+ - REST responses echo the selected variant under `x_decision_toolkit.variant`;
46
+ a fixed injected model rejects a conflicting request variant.
47
+ - Added cross-surface tests for library resolution, CLI pass-through, registry
48
+ defaults and System One/OpenAI REST requests, plus documentation for selection
49
+ precedence.
50
+
6
51
  ## [0.1.6] — 2026-10-20
7
52
 
8
53
  Variant catalog and selection release. `dtk models list` now exposes every
@@ -106,7 +151,7 @@ README correction. No code or model behavior changed.
106
151
 
107
152
  ### Phase 2 — REST
108
153
 
109
- - Versioned System One 1.14 and OpenAI Decisions beta projections.
154
+ - Versioned System One and OpenAI Decisions beta projections.
110
155
  - Bearer auth, per-request model/variant, usage, standard error envelope.
111
156
  - OpenAI-shaped `/v1/models`, rich `/v1/decision-models`, readiness `503`.
112
157
  - Request ids, access logs, body limits, CORS and timeouts.
@@ -116,6 +161,8 @@ README correction. No code or model behavior changed.
116
161
  - Typed `DecisionModel` API with lifecycle, batching, async, usage, warnings,
117
162
  normalized confidence, capabilities, structured events and atomic model store.
118
163
 
164
+ [0.2.0]: https://pypi.org/project/decision-toolkit/0.2.0/
165
+ [0.1.7]: https://pypi.org/project/decision-toolkit/0.1.7/
119
166
  [0.1.6]: https://pypi.org/project/decision-toolkit/0.1.6/
120
167
  [0.1.5]: https://pypi.org/project/decision-toolkit/0.1.5/
121
168
  [0.1.4]: https://test.pypi.org/project/decision-toolkit/0.1.4/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: decision-toolkit
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: Local-first toolkit for open-weights decision models: one ONNX engine, one adapter per model contract, typed choice/yes-no/score decisions with probabilities on CPU.
5
5
  Project-URL: Homepage, https://github.com/gia-uh/decision-toolkit
6
6
  Project-URL: Repository, https://github.com/gia-uh/decision-toolkit
@@ -59,7 +59,7 @@ and CLI. `dtk --help` and every subcommand help also include runnable examples.
59
59
  ## Installation
60
60
 
61
61
  ```bash
62
- uv pip install decision-toolkit # library + CLI (0.1.6 on PyPI)
62
+ uv pip install decision-toolkit # library + CLI (0.2.0 on PyPI)
63
63
  uv pip install "decision-toolkit[onnx]" # + onnxruntime, tokenizers (the model runtime)
64
64
  uv pip install "decision-toolkit[server]" # + REST server (FastAPI)
65
65
  ```
@@ -67,7 +67,7 @@ uv pip install "decision-toolkit[server]" # + REST server (FastAPI)
67
67
  The project is managed with [uv](https://docs.astral.sh/uv/) and ships a committed `uv.lock`. Published
68
68
  wheels remain compatible with `pip` for environments that do not use uv.
69
69
 
70
- PyPI: <https://pypi.org/project/decision-toolkit/0.1.6/>. See
70
+ PyPI: <https://pypi.org/project/decision-toolkit/0.2.0/>. See
71
71
  [docs/RELEASE.md](https://github.com/gia-uh/decision-toolkit/blob/main/docs/RELEASE.md)
72
72
  for the Trusted Publishing flow and clean-install checks.
73
73
 
@@ -173,12 +173,16 @@ dtk serve --model gliner2.5-multi --warmup \
173
173
  --log-format json
174
174
  ```
175
175
 
176
- Endpoints: `POST /v1/systemone` (System One 1.14), `POST /v1/decisions`
176
+ Endpoints: `POST /v1/systemone` (TypeSafe System One), `POST /v1/decisions`
177
177
  (OpenAI Decisions beta), `GET /v1/models` (OpenAI shape),
178
178
  `GET /v1/decision-models` (rich catalog), `GET /v1/info`, `GET /health`,
179
- `GET /ready`. Requests may select `model` and `variant`; failures use the OpenAI
180
- `{"error": {...}}` envelope and every response carries `x-request-id`.
181
- `TYPESAFE_BASE_URL=http://localhost:8000` makes existing System One clients work.
179
+ `GET /ready`. Requests select `model`; `variant` is a documented local extension,
180
+ not part of either public API. System One request validation uses the TypeSafe
181
+ `422 {"detail": [...]}` shape; OpenAI Decisions uses its
182
+ `{"error": {...}}` envelope. Every response carries `x-request-id`.
183
+ A standard System One client that sends `model`, `state` and `questions` can
184
+ point at `http://localhost:8000`; image/asset and `box` requests are outside the
185
+ current text-only subset.
182
186
  See [docs/REST.md](https://github.com/gia-uh/decision-toolkit/blob/main/docs/REST.md) for the pinned contracts, limits, auth and
183
187
  the standard-evolution process.
184
188
 
@@ -241,9 +245,11 @@ dtk models current --json # {"current": "...", "variant": "fp32"}
241
245
  dtk decide --model gliner2.5-multi --variant fp16 --state "..." --q 'cat=choice:a|b'
242
246
  ```
243
247
 
244
- From Python: `DecisionModel("gliner2.5-multi", variant="fp32")`. The variant
245
- must already be downloaded. If a variant is omitted, the catalog default is
246
- used unless `dtk models use` selected another one.
248
+ From Python: `DecisionModel("gliner2.5-multi", variant="fp32")`. REST selects
249
+ the same way with `"variant": "fp32"` in the request body and returns the choice
250
+ under `x_decision_toolkit.variant`. The variant must already be downloaded. If a
251
+ variant is omitted, the catalog default is used unless `dtk models use` selected
252
+ another one.
247
253
 
248
254
  A variant becomes default only after passing the parity gate (top-1 ≥ 99%,
249
255
  mean |Δp| ≤ 0.02, accuracy delta ≤ 1 pp against the fp32 build on a fixed
@@ -21,7 +21,7 @@ and CLI. `dtk --help` and every subcommand help also include runnable examples.
21
21
  ## Installation
22
22
 
23
23
  ```bash
24
- uv pip install decision-toolkit # library + CLI (0.1.6 on PyPI)
24
+ uv pip install decision-toolkit # library + CLI (0.2.0 on PyPI)
25
25
  uv pip install "decision-toolkit[onnx]" # + onnxruntime, tokenizers (the model runtime)
26
26
  uv pip install "decision-toolkit[server]" # + REST server (FastAPI)
27
27
  ```
@@ -29,7 +29,7 @@ uv pip install "decision-toolkit[server]" # + REST server (FastAPI)
29
29
  The project is managed with [uv](https://docs.astral.sh/uv/) and ships a committed `uv.lock`. Published
30
30
  wheels remain compatible with `pip` for environments that do not use uv.
31
31
 
32
- PyPI: <https://pypi.org/project/decision-toolkit/0.1.6/>. See
32
+ PyPI: <https://pypi.org/project/decision-toolkit/0.2.0/>. See
33
33
  [docs/RELEASE.md](https://github.com/gia-uh/decision-toolkit/blob/main/docs/RELEASE.md)
34
34
  for the Trusted Publishing flow and clean-install checks.
35
35
 
@@ -135,12 +135,16 @@ dtk serve --model gliner2.5-multi --warmup \
135
135
  --log-format json
136
136
  ```
137
137
 
138
- Endpoints: `POST /v1/systemone` (System One 1.14), `POST /v1/decisions`
138
+ Endpoints: `POST /v1/systemone` (TypeSafe System One), `POST /v1/decisions`
139
139
  (OpenAI Decisions beta), `GET /v1/models` (OpenAI shape),
140
140
  `GET /v1/decision-models` (rich catalog), `GET /v1/info`, `GET /health`,
141
- `GET /ready`. Requests may select `model` and `variant`; failures use the OpenAI
142
- `{"error": {...}}` envelope and every response carries `x-request-id`.
143
- `TYPESAFE_BASE_URL=http://localhost:8000` makes existing System One clients work.
141
+ `GET /ready`. Requests select `model`; `variant` is a documented local extension,
142
+ not part of either public API. System One request validation uses the TypeSafe
143
+ `422 {"detail": [...]}` shape; OpenAI Decisions uses its
144
+ `{"error": {...}}` envelope. Every response carries `x-request-id`.
145
+ A standard System One client that sends `model`, `state` and `questions` can
146
+ point at `http://localhost:8000`; image/asset and `box` requests are outside the
147
+ current text-only subset.
144
148
  See [docs/REST.md](https://github.com/gia-uh/decision-toolkit/blob/main/docs/REST.md) for the pinned contracts, limits, auth and
145
149
  the standard-evolution process.
146
150
 
@@ -203,9 +207,11 @@ dtk models current --json # {"current": "...", "variant": "fp32"}
203
207
  dtk decide --model gliner2.5-multi --variant fp16 --state "..." --q 'cat=choice:a|b'
204
208
  ```
205
209
 
206
- From Python: `DecisionModel("gliner2.5-multi", variant="fp32")`. The variant
207
- must already be downloaded. If a variant is omitted, the catalog default is
208
- used unless `dtk models use` selected another one.
210
+ From Python: `DecisionModel("gliner2.5-multi", variant="fp32")`. REST selects
211
+ the same way with `"variant": "fp32"` in the request body and returns the choice
212
+ under `x_decision_toolkit.variant`. The variant must already be downloaded. If a
213
+ variant is omitted, the catalog default is used unless `dtk models use` selected
214
+ another one.
209
215
 
210
216
  A variant becomes default only after passing the parity gate (top-1 ≥ 99%,
211
217
  mean |Δp| ≤ 0.02, accuracy delta ≤ 1 pp against the fp32 build on a fixed
@@ -1,7 +1,7 @@
1
1
  # Core programmatic API (phase 1)
2
2
 
3
3
  The programmatic API is the **superset**: the most expressive layer of the
4
- toolkit. The REST server is a standards-compliant projection of a subset of
4
+ toolkit. The REST server is a documented, conformance-tested projection of a subset of
5
5
  this API, and the CLI is our own operational subset. Nothing in this document
6
6
  belongs to a wire contract.
7
7
 
@@ -132,6 +132,7 @@ dtk serve --model gliner2.5-multi --warmup --port 8000
132
132
  curl -s http://127.0.0.1:8000/v1/systemone \
133
133
  -H "Content-Type: application/json" \
134
134
  -d '{
135
+ "model": "gliner2.5-multi",
135
136
  "state": "Meeting with Acme: 10% discount, sign next week.",
136
137
  "questions": {
137
138
  "cat": {
@@ -150,6 +151,7 @@ curl -s http://127.0.0.1:8000/v1/systemone \
150
151
  curl -s http://127.0.0.1:8000/v1/decisions \
151
152
  -H "Content-Type: application/json" \
152
153
  -d '{
154
+ "model": "gliner2.5-multi",
153
155
  "input": "Please change my seat to an aisle.",
154
156
  "questions": [{
155
157
  "type": "choice",
@@ -163,6 +165,8 @@ curl -s http://127.0.0.1:8000/v1/decisions \
163
165
  }' | jq
164
166
  ```
165
167
 
168
+ `variant` is a decision-toolkit extension; omit it for a standard client.
169
+
166
170
  ### Model per request and auth
167
171
 
168
172
  ```bash
@@ -6,7 +6,9 @@ file was written, so the name was available). The first release was `0.1.0`;
6
6
  `0.1.3` added release verification and docs publication; `0.1.4` migrated
7
7
  package management and builds to uv but was published to TestPyPI only; `0.1.5`
8
8
  fixed TestPyPI verification for uv; `0.1.6` added complete variant visibility
9
- and selection in the CLI and documentation.
9
+ and selection in the CLI and documentation; `0.1.7` aligned variant selection
10
+ across the library, CLI and REST API; `0.2.0` corrected the REST conformance
11
+ claims and wire shapes to the published TypeSafe and OpenAI schemas.
10
12
 
11
13
  ## Local build and verification
12
14
 
@@ -33,7 +35,7 @@ Clean install test (no `PYTHONPATH`):
33
35
  ```bash
34
36
  uv venv --python 3.12 /tmp/dtk-wheel
35
37
  uv pip install --python /tmp/dtk-wheel/bin/python \
36
- dist/decision_toolkit-0.1.6-py3-none-any.whl
38
+ dist/decision_toolkit-0.2.0-py3-none-any.whl
37
39
  uv pip check --python /tmp/dtk-wheel/bin/python
38
40
  /tmp/dtk-wheel/bin/dtk --version
39
41
  /tmp/dtk-wheel/bin/dtk models list --json
@@ -44,7 +46,7 @@ With inference and server extras:
44
46
 
45
47
  ```bash
46
48
  uv pip install --python /tmp/dtk-wheel/bin/python \
47
- "dist/decision_toolkit-0.1.6-py3-none-any.whl[onnx,server,tracing]"
49
+ "dist/decision_toolkit-0.2.0-py3-none-any.whl[onnx,server,tracing]"
48
50
  /tmp/dtk-wheel/bin/dtk decide --model gliner2.5-decide ...
49
51
  /tmp/dtk-wheel/bin/dtk serve --model gliner2.5-decide --warmup --port 8016
50
52
  curl -s http://127.0.0.1:8016/health
@@ -78,8 +80,8 @@ No long-lived PyPI token is needed; the workflow uses GitHub OIDC
78
80
  4. Create and push an annotated tag:
79
81
 
80
82
  ```bash
81
- git tag -a v0.1.6 -m "decision-toolkit 0.1.6"
82
- git push origin v0.1.6
83
+ git tag -a v0.2.0 -m "decision-toolkit 0.2.0"
84
+ git push origin v0.2.0
83
85
  ```
84
86
 
85
87
  5. `.github/workflows/release.yml` builds once, publishes to **TestPyPI**, then
@@ -128,16 +130,18 @@ to TestPyPI without publishing to PyPI or creating a GitHub release.
128
130
 
129
131
  ## Published release
130
132
 
131
- `0.1.6` is published through Trusted Publishing on 2026-10-20:
133
+ `0.2.0` is published through Trusted Publishing on 2026-10-20:
132
134
 
133
- <https://pypi.org/project/decision-toolkit/0.1.6/>
135
+ <https://pypi.org/project/decision-toolkit/0.2.0/>
134
136
 
135
137
  `0.1.0` was the first release; `0.1.1` refreshed the PyPI description; `0.1.2`
136
138
  validated the OIDC TestPyPI → PyPI flow; `0.1.3` added post-publish install
137
139
  verification and the GitHub Pages documentation site; `0.1.4` moved the
138
140
  development, build, CI, release and documentation environments to uv but was
139
141
  published to TestPyPI only; `0.1.5` fixed TestPyPI resolution; `0.1.6` added
140
- complete variant/quantization visibility and selection.
142
+ complete variant/quantization visibility and CLI selection; `0.1.7` aligned
143
+ selection across library, CLI and REST; `0.2.0` corrected the REST conformance
144
+ claims and wire shapes.
141
145
 
142
146
  The tag-triggered workflow remains the preferred path for future releases once
143
147
  the TestPyPI/PyPI trusted publishers and protected environments are configured.
@@ -1,37 +1,42 @@
1
1
  # REST projection (phase 2)
2
2
 
3
- REST is a **standards-compliant projection** of the programmatic core, not a
4
- second implementation. The core remains the superset; the server selects a
5
- subset, maps it to a pinned wire contract, and adds transport concerns (auth,
6
- request ids, limits, CORS, logging).
3
+ REST is a **documented projection** of the programmatic core, not a second
4
+ implementation. The core remains the superset; the server selects a supported
5
+ subset, maps it to the published wire contracts, and adds transport concerns
6
+ (auth, request ids, limits, CORS, logging).
7
7
 
8
8
  ```
9
9
  Programmatic core (superset)
10
- │ pure projection + conformance
10
+ │ pure projection + conformance tests
11
11
  ▼
12
- REST (subset; System One 1.14 + OpenAI Decisions beta)
12
+ REST (supported subset; TypeSafe System One + OpenAI Decisions beta)
13
13
  ```
14
14
 
15
+ Compatibility is tested against the published shapes, not against a local
16
+ invention. The current models are text-only, so image/assets and `box` requests
17
+ are explicitly rejected rather than silently approximated.
18
+
15
19
  The wire models live in `decision_toolkit/contracts/`, the multi-model registry
16
20
  in `decision_toolkit/registry.py`, and `server.py` contains only transport and
17
21
  FastAPI glue.
18
22
 
19
23
  ## Pinned contracts
20
24
 
21
- | Contract | Version | Module |
22
- | --- | --- | --- |
23
- | System One (TypeSafe-compatible) | `1.14` | `contracts/systemone_v1.py` |
24
- | OpenAI Decisions | `beta` | `contracts/openai_decisions_beta.py` |
25
+ | Contract | Version | Reference | Module |
26
+ | --- | --- | --- | --- |
27
+ | TypeSafe System One | API schema `0.2.0` | <https://docs.typesafe.ai/api> | `contracts/systemone_v1.py` |
28
+ | OpenAI Decisions | `beta` | <https://developers.openai.com/api/docs/guides/decisions> | `contracts/openai_decisions_beta.py` |
25
29
 
26
- `GET /v1/info` exposes both versions. The reference for System One is the
27
- TypeSafe 1.14 API as implemented by llama.cpp's `POST /v1/systemone`; the
28
- OpenAI dialect is the beta Decisions shape already translated by the toolkit.
30
+ `GET /v1/info` exposes both versions. "System One 1.14" was an incorrect label
31
+ in earlier documentation; the published TypeSafe API schema is `0.2.0`. The
32
+ implementation targets that schema and the published OpenAI beta shape, with the
33
+ limitations listed below.
29
34
 
30
35
  ## Endpoints
31
36
 
32
37
  | Method | Path | Purpose |
33
38
  | --- | --- | --- |
34
- | `POST` | `/v1/systemone` | System One 1.14 decision. |
39
+ | `POST` | `/v1/systemone` | TypeSafe System One decision. |
35
40
  | `POST` | `/v1/decisions` | OpenAI Decisions beta decision. |
36
41
  | `GET` | `/v1/models` | OpenAI model-list shape. |
37
42
  | `GET` | `/v1/models/{id}` | One model in the OpenAI shape. |
@@ -44,12 +49,33 @@ OpenAI dialect is the beta Decisions shape already translated by the toolkit.
44
49
  Toolkit-only metadata is under `x_decision_toolkit`, never mixed into standard
45
50
  fields. `/v1/decision-models` is the rich, non-standard catalog endpoint.
46
51
 
47
- ## System One 1.14
52
+ ## Variants (local extension)
48
53
 
49
- Request:
54
+ Neither TypeSafe nor OpenAI has a `variant` request field: the public APIs select
55
+ a model by name. Quantization is a **decision-toolkit extension**:
56
+
57
+ - Request `variant` wins when a local client sends it.
58
+ - Otherwise, a server started with `dtk serve --variant` uses that default.
59
+ - Otherwise, the variant selected with `dtk models use` is used when the server
60
+ is serving the current model.
61
+ - Otherwise, the catalog default (the `*` variant in `dtk models list`) is used.
62
+
63
+ Standard clients should omit `variant`; they work with the server default. A
64
+ standard-only integration can run one server per variant, or use the server's
65
+ `--variant` setting. The selected variant is exposed in the namespaced
66
+ `x_decision_toolkit.variant` extension, never in a standard field.
67
+ `/v1/models`, `/v1/models/{id}`, `/v1/decision-models`, `/v1/info` and `/ready`
68
+ expose the configured catalog variants, the default, cached variants, and the
69
+ current selection. See [Variants and quantization](VARIANTS.md) for the catalog
70
+ and download/select commands.
71
+
72
+ ## System One (TypeSafe API schema 0.2.0)
73
+
74
+ Request (standard fields, plus the optional local `model` routing extension):
50
75
 
51
76
  ```json
52
77
  {
78
+ "model": "gliner2.5-multi",
53
79
  "state": "I was charged twice and nobody replied.",
54
80
  "questions": {
55
81
  "route": {
@@ -88,9 +114,9 @@ Response:
88
114
  "confidence": 0.4
89
115
  }
90
116
  },
91
- "usage": {"input_tokens": 239, "output_tokens": 0},
117
+ "usage": {"input_tokens": 239, "output_tokens": 12},
92
118
  "x_decision_toolkit": {
93
- "spec": "1.14",
119
+ "spec": "0.2.0",
94
120
  "request_id": "req-123",
95
121
  "variant": "fp16",
96
122
  "adapter": "gliner2",
@@ -105,16 +131,23 @@ Response:
105
131
 
106
132
  Standard fields:
107
133
 
108
- - `usage.output_tokens` is **always 0** (decisions generate no text).
134
+ - `usage.output_tokens` is a non-negative count. The current core reports the
135
+ tokenizer count of the serialized answers (it may be non-zero); do not assume
136
+ it is always zero.
109
137
  - `usage.input_tokens` comes from the core token accounting.
110
138
  - The full core usage, including the explicit `estimated` flag, is under
111
139
  `x_decision_toolkit.usage`.
112
- - `choice` requires a non-empty criteria object; `score` needs 2–10 levels;
113
- `noul` may omit criteria. Invalid bodies return `400`.
114
- - `files`/`images` accept data URLs only. Text-only models return `501` with
115
- `code: "multimodal_not_supported"`; malformed/non-data URLs return `400`.
116
- - `model` and `variant` are optional routing extensions for a multi-model local
117
- server. An unqualified request uses the configured default model.
140
+ - `choice` requires a non-empty criteria object; System One `score` accepts
141
+ `1`–`10` ordered levels; `noul` may omit criteria.
142
+ - Request-schema and question-validation failures return `422` with the
143
+ FastAPI-style `{"detail": [...]}` body used by the TypeSafe schema. Model
144
+ lookup, capability and transport failures keep their HTTP status and the
145
+ documented error envelope.
146
+ - Standard `assets` are accepted by the schema but return `501` because the
147
+ current catalog is text-only. Legacy `files`/`images` fields are local
148
+ extensions; valid data URLs also return `501`, malformed values return `422`.
149
+ - `model` is optional only as a local convenience; standard clients send it.
150
+ `variant` is a local extension, not part of the TypeSafe schema.
118
151
 
119
152
  ## OpenAI Decisions beta
120
153
 
@@ -130,9 +163,7 @@ Request:
130
163
  {"type": "score", "name": "urgency", "instructions": "How urgent?",
131
164
  "levels": [{"label": "low"}, {"label": "high"}]}
132
165
  ],
133
- "safety_identifier": "user-hash",
134
- "model": "gliner2.5-multi",
135
- "variant": "fp16"
166
+ "model": "gliner2.5-multi"
136
167
  }
137
168
  ```
138
169
 
@@ -140,10 +171,6 @@ Response:
140
171
 
141
172
  ```json
142
173
  {
143
- "id": "req-123",
144
- "object": "response",
145
- "created_at": 1790000000,
146
- "status": "completed",
147
174
  "model": "gliner2.5-multi",
148
175
  "answers": [
149
176
  {"type": "predicate", "name": "refund", "probability": 0.93},
@@ -156,8 +183,13 @@ Response:
156
183
  {"value": 1, "label": "high", "probability": 0.8}],
157
184
  "confidence": 0.8}
158
185
  ],
159
- "usage": {"input_tokens": 92, "output_tokens": 0, "total_tokens": 92},
160
- "safety_identifier": "user-hash",
186
+ "usage": {
187
+ "input_tokens": 92,
188
+ "output_tokens": 12,
189
+ "total_tokens": 104,
190
+ "input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
191
+ "output_tokens_details": {"reasoning_tokens": 0}
192
+ },
161
193
  "x_decision_toolkit": {
162
194
  "spec": "beta",
163
195
  "request_id": "req-123",
@@ -166,15 +198,20 @@ Response:
166
198
  "latency_ms": 350.1,
167
199
  "truncated": false,
168
200
  "warnings": [],
169
- "usage": {"input_tokens": 92, "output_tokens": 10, "total_tokens": 102, "estimated": true}
201
+ "usage": {"input_tokens": 92, "output_tokens": 12, "total_tokens": 104, "estimated": true}
170
202
  }
171
203
  }
172
204
  ```
173
205
 
174
- - Original choice values (numbers, booleans, strings) are echoed back, not the
175
- internal System One labels.
206
+ - The public beta response fields are `model`, `answers` and `usage`; there is
207
+ no invented `id`, `object`, `created_at`, `status` or `safety_identifier`.
208
+ - Original choice values are echoed back, not the internal System One labels.
209
+ The published beta accepts string/boolean values; the toolkit also accepts
210
+ numbers as a local extension.
176
211
  - Duplicate question names return `400`.
177
- - Images in `input` return `501` (`multimodal_not_supported`).
212
+ - Images in `input` return `501` (`multimodal_not_supported`); the published
213
+ beta supports inline images, but the current catalog has no vision model.
214
+ - `variant` is a local extension; standard clients omit it.
178
215
  - OpenAI SDK clients can list models (`client.models.list()`) and parse the
179
216
  error envelope (`{"error": {message, type, param, code}}`).
180
217
 
@@ -206,9 +243,11 @@ A model is not "ready" merely because it is configured:
206
243
 
207
244
  ## Limits and transport
208
245
 
209
- - 1–128 questions.
210
- - 1–255 choice options; 2–10 score levels.
211
- - 8 files/images maximum (the current catalog supports 0).
246
+ - 1–128 questions (local limit; the TypeSafe schema has no maximum).
247
+ - 1–255 choice options; 1–10 System One score levels; OpenAI score levels
248
+ require at least 2.
249
+ - 8 legacy files/images maximum (the current catalog supports 0); standard
250
+ `assets` and images are rejected with `501`.
212
251
  - Request body limit (`--max-body-mb`, default 16) returns `413`.
213
252
  - Per-decision timeout (`--timeout`, default 120 s) returns `504`.
214
253
  - CORS origins are explicit (`--cors-origin`, repeatable).
@@ -219,22 +258,22 @@ A model is not "ready" merely because it is configured:
219
258
 
220
259
  ## Error mapping
221
260
 
222
- All errors use `{"error": {"message", "type", "param", "code"}}`.
261
+ `/v1/systemone` request-validation failures follow the TypeSafe schema:
262
+ `422` with `{"detail": [...]}`. `/v1/decisions` validation and toolkit domain
263
+ errors use the OpenAI envelope
264
+ `{"error": {"message", "type", "param", "code"}}`.
223
265
 
224
- | Condition | HTTP | code |
266
+ | Condition | System One | OpenAI Decisions |
225
267
  | --- | --- | --- |
226
- | Invalid question/body | `400` | `invalid_question` / `invalid_request` |
227
- | Unsupported capability (`box`, assets) | `400` | `unsupported_capability` |
228
- | Unknown model | `404` | `model_not_found` |
229
- | Model not downloaded | `503` | `model_not_downloaded` |
230
- | Adapter unavailable | `503` | `adapter_not_available` |
231
- | Adapter not implemented | `501` | `adapter_not_implemented` |
232
- | Images/audio unsupported | `501` | `multimodal_not_supported` |
233
- | Truncation in strict mode | `400` | `truncated_input` |
234
- | Body too large | `413` | `request_too_large` |
235
- | Missing/invalid bearer | `401` | `authentication_error` |
236
- | Timeout | `504` | `timeout` |
237
- | Backend failure | `500` | `backend_failure` / `adapter_error` |
268
+ | Request-schema validation | `422` + `detail` | `400` + error envelope |
269
+ | Toolkit question validation | `422` + `detail` | `400` + error envelope |
270
+ | Unknown model | `404` | `404` |
271
+ | Model not downloaded | `503` | `503` |
272
+ | Unsupported `box` / assets / images | `501` | `501` |
273
+ | Truncation in strict mode | `400` | `400` |
274
+ | Body too large | `413` | `413` |
275
+ | Missing/invalid bearer | `401` | `401` |
276
+ | Timeout | `504` | `504` |
238
277
 
239
278
  The CLI/REST never parse error strings to decide behavior; they use the stable
240
279
  `code`.
@@ -261,16 +300,17 @@ Adding a new spec version means adding a new module, not editing the old one.
261
300
 
262
301
  ## Compatibility matrix (current)
263
302
 
264
- | Feature | System One 1.14 | OpenAI Decisions beta |
303
+ | Feature | System One (TypeSafe 0.2.0) | OpenAI Decisions beta |
265
304
  | --- | --- | --- |
266
305
  | `choice` / `noul` / `score` | yes | `choice` / `predicate` / `score` |
267
306
  | `box` | typed by core; rejected by current text models | no |
268
307
  | Text state | yes | `input` string |
269
308
  | JSON state | yes | message/string `input` |
270
- | Images | `501` for current models | `501` for current models |
271
- | Usage | `input_tokens` + `output_tokens=0` | input/output/total |
272
- | Per-request model/variant | extension | `model` + `variant` extension |
273
- | Errors | OpenAI-style envelope | OpenAI envelope |
309
+ | Images/assets | `501` for current models | `501` for current models |
310
+ | Usage | `input_tokens` + non-negative `output_tokens` | input/output/total + details |
311
+ | Per-request model | standard field; optional local default | standard field; optional local default |
312
+ | `variant` | local extension | local extension |
313
+ | Validation errors | `422` + `detail` | `400` + envelope |
274
314
  | Auth / CORS / limits | server-level | server-level |
275
315
 
276
316
  ## Running
@@ -287,8 +327,8 @@ at `http://localhost:8000/v1`.
287
327
 
288
328
  ## Testing
289
329
 
290
- - System One contract tests cover the exact request/response fields, usage
291
- `output_tokens=0`, invalid request matrix and image status codes.
330
+ - System One contract tests cover the published request/response fields,
331
+ non-negative usage, the invalid request matrix and image/asset status codes.
292
332
  - OpenAI contract tests use the official `openai` Python SDK for
293
333
  `models.list()` and error-envelope parsing.
294
334
  - Equivalence tests send the same core decision through HTTP and compare
@@ -32,6 +32,34 @@ Models with a single row have only one catalogued, verified build. That does
32
32
  not mean other conversions do not exist upstream; it means they have not passed
33
33
  the parity, hash and license gates for this catalog.
34
34
 
35
+ ## One selection model across surfaces
36
+
37
+ The same catalog variant is used by the library, CLI and REST API. Selection
38
+ precedence is:
39
+
40
+ 1. an explicit per-call/per-request variant (`DecisionModel(..., variant=...)`,
41
+ `--variant`, or `"variant"` in a REST body);
42
+ 2. the CLI/server configuration (`dtk models use`, or `dtk serve --variant`);
43
+ 3. the catalog default marked with `*`.
44
+
45
+ | Surface | Explicit selection | Default when omitted |
46
+ | --- | --- | --- |
47
+ | Library | `DecisionModel("gliner2.5-multi", variant="fp32")` | persisted variant for the current model, else catalog default |
48
+ | CLI | `--variant fp32` | variant selected by `dtk models use`, else catalog default |
49
+ | REST (local extension) | `"variant": "fp32"` in the request body | server `--variant`, else persisted variant for the current model, else catalog default |
50
+
51
+ `DecisionModel.variant` and the CLI report the selected variant (`id@variant` in
52
+ `dtk models current`). REST responses expose it under the namespaced
53
+ `x_decision_toolkit.variant` extension; `/v1/decision-models` includes
54
+ `current` and `current_variant`, plus all `variants` and `cached_variants`.
55
+ `variant` is **not** part of TypeSafe or OpenAI; standard clients should omit it
56
+ and rely on the server's `--variant`/persisted default. A server started with
57
+ `--model`/`--variant` reports that default in `/v1/info` and `/ready`.
58
+
59
+ The variant must have been downloaded before a library call or REST request
60
+ loads it. An unknown variant returns a model-not-found error; a known but
61
+ missing cache entry returns a model-not-downloaded error.
62
+
35
63
  ## Inspect the catalog
36
64
 
37
65
  ```bash
@@ -18,7 +18,7 @@ PyPI: <https://pypi.org/project/decision-toolkit/>
18
18
 
19
19
  - [Examples](EXAMPLES.md) — library, REST and CLI.
20
20
  - [Core API](CORE.md) — the superset used by every surface.
21
- - [REST](REST.md) — System One 1.14 + OpenAI Decisions beta.
21
+ - [REST](REST.md) — TypeSafe System One + OpenAI Decisions beta.
22
22
  - [CLI](CLI.md) — operational commands and batch workflows.
23
23
  - [Variants and quantization](VARIANTS.md) — every catalogued build and how to download/select it.
24
24
  - [Operations](OPERATIONS.md) — rate limits, metrics, tracing and runbook.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "decision-toolkit"
7
- version = "0.1.6"
7
+ version = "0.2.0"
8
8
  description = "Local-first toolkit for open-weights decision models: one ONNX engine, one adapter per model contract, typed choice/yes-no/score decisions with probabilities on CPU."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -7,7 +7,7 @@ questions with a calibrated probability for every option, on CPU.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- __version__ = "0.1.6"
10
+ __version__ = "0.2.0"
11
11
 
12
12
  from .adapters import register_adapter
13
13
  from .calibration import brier_score, expected_calibration_error, threshold_sweep