REST projection (phase 2)¶
REST is a documented projection of the programmatic core, not a second implementation. The core remains the superset; the server selects a supported subset, maps it to the published wire contracts, and adds transport concerns (auth, request ids, limits, CORS, logging).
Programmatic core (superset)
│ pure projection + conformance tests
▼
REST (supported subset; TypeSafe System One + OpenAI Decisions beta)
Compatibility is tested against the published shapes, not against a local
invention. The current models are text-only, so image/assets and box requests
are explicitly rejected rather than silently approximated.
The wire models live in decision_toolkit/contracts/, the multi-model registry
in decision_toolkit/registry.py, and server.py contains only transport and
FastAPI glue.
Pinned contracts¶
| Contract | Version | Reference | Module |
|---|---|---|---|
| TypeSafe System One | API schema 0.2.0 |
https://docs.typesafe.ai/api | contracts/systemone_v1.py |
| OpenAI Decisions | beta |
https://developers.openai.com/api/docs/guides/decisions | contracts/openai_decisions_beta.py |
GET /v1/info exposes both versions. "System One 1.14" was an incorrect label
in earlier documentation; the published TypeSafe API schema is 0.2.0. The
implementation targets that schema and the published OpenAI beta shape, with the
limitations listed below.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/systemone |
TypeSafe System One decision. |
POST |
/v1/decisions |
OpenAI Decisions beta decision. |
GET |
/v1/models |
OpenAI model-list shape. |
GET |
/v1/models/{id} |
One model in the OpenAI shape. |
GET |
/v1/decision-models |
Rich catalog: variants, licenses, capabilities, cache. |
GET |
/v1/info |
Version, contracts, registry state, auth mode. |
GET |
/health, /v1/health |
Liveness, always 200. |
GET |
/ready, /v1/ready |
200 only when the default model is warm; 503 otherwise. |
/v1/models and /v1/models/{id} are the OpenAI-compatible surface.
Toolkit-only metadata is under x_decision_toolkit, never mixed into standard
fields. /v1/decision-models is the rich, non-standard catalog endpoint.
Variants (local extension)¶
Neither TypeSafe nor OpenAI has a variant request field: the public APIs select
a model by name. Quantization is a decision-toolkit extension:
- Request
variantwins when a local client sends it. - Otherwise, a server started with
dtk serve --variantuses that default. - Otherwise, the variant selected with
dtk models useis used when the server is serving the current model. - Otherwise, the catalog default (the
*variant indtk models list) is used.
Standard clients should omit variant; they work with the server default. A
standard-only integration can run one server per variant, or use the server's
--variant setting. The selected variant is exposed in the namespaced
x_decision_toolkit.variant extension, never in a standard field.
/v1/models, /v1/models/{id}, /v1/decision-models, /v1/info and /ready
expose the configured catalog variants, the default, cached variants, and the
current selection. See Variants and quantization for the catalog
and download/select commands.
System One (TypeSafe API schema 0.2.0)¶
Request (standard fields, plus the optional local model routing extension):
{
"model": "gliner2.5-multi",
"state": "I was charged twice and nobody replied.",
"questions": {
"route": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {"billing": "payments", "shipping": null, "technical": null}
},
"angry": {"type": "noul", "instructions": "Is the customer angry?"},
"urgency": {
"type": "score",
"instructions": "How urgent?",
"criteria": ["can wait", "this week", "today"]
}
}
}
Response:
{
"model": "gliner2.5-multi",
"answers": {
"route": {
"type": "choice",
"choice": "billing",
"probabilities": {"billing": 0.82, "shipping": 0.11, "technical": 0.07},
"confidence": 0.82
},
"angry": {"type": "noul", "noul": 0.61},
"urgency": {
"type": "score",
"score": 1.2,
"legend": {"0": "can wait", "1": "this week", "2": "today"},
"probabilities": {"0": 0.2, "1": 0.4, "2": 0.4},
"confidence": 0.4
}
},
"usage": {"input_tokens": 239, "output_tokens": 12},
"x_decision_toolkit": {
"spec": "0.2.0",
"request_id": "req-123",
"variant": "fp16",
"adapter": "gliner2",
"latency_ms": 353.2,
"truncated": false,
"warnings": [],
"usage": {"input_tokens": 239, "output_tokens": 12, "total_tokens": 251, "estimated": true},
"answers": {"route": {"confidence_kind": "probability", "truncated": false}}
}
}
Standard fields:
usage.output_tokensis a non-negative count. The current core reports the tokenizer count of the serialized answers (it may be non-zero); do not assume it is always zero.usage.input_tokenscomes from the core token accounting.- The full core usage, including the explicit
estimatedflag, is underx_decision_toolkit.usage. choicerequires a non-empty criteria object; System Onescoreaccepts1–10ordered levels;noulmay omit criteria.- Request-schema and question-validation failures return
422with the FastAPI-style{"detail": [...]}body used by the TypeSafe schema. Model lookup, capability and transport failures keep their HTTP status and the documented error envelope. - Standard
assetsare accepted by the schema but return501because the current catalog is text-only. Legacyfiles/imagesfields are local extensions; valid data URLs also return501, malformed values return422. modelis optional only as a local convenience; standard clients send it.variantis a local extension, not part of the TypeSafe schema.
OpenAI Decisions beta¶
Request:
{
"input": "Customer wants a refund.",
"questions": [
{"type": "predicate", "name": "refund", "instructions": "Is it a refund?"},
{"type": "choice", "name": "team", "instructions": "Which team?",
"choices": [{"value": "billing"}, {"value": "tech"}]},
{"type": "score", "name": "urgency", "instructions": "How urgent?",
"levels": [{"label": "low"}, {"label": "high"}]}
],
"model": "gliner2.5-multi"
}
Response:
{
"model": "gliner2.5-multi",
"answers": [
{"type": "predicate", "name": "refund", "probability": 0.93},
{"type": "choice", "name": "team", "choice": "billing",
"probabilities": [{"value": "billing", "probability": 0.91},
{"value": "tech", "probability": 0.09}],
"confidence": 0.91},
{"type": "score", "name": "urgency", "score": 0.8,
"probabilities": [{"value": 0, "label": "low", "probability": 0.2},
{"value": 1, "label": "high", "probability": 0.8}],
"confidence": 0.8}
],
"usage": {
"input_tokens": 92,
"output_tokens": 12,
"total_tokens": 104,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens_details": {"reasoning_tokens": 0}
},
"x_decision_toolkit": {
"spec": "beta",
"request_id": "req-123",
"variant": "fp16",
"adapter": "gliner2",
"latency_ms": 350.1,
"truncated": false,
"warnings": [],
"usage": {"input_tokens": 92, "output_tokens": 12, "total_tokens": 104, "estimated": true}
}
}
- The public beta response fields are
model,answersandusage; there is no inventedid,object,created_at,statusorsafety_identifier. - Original choice values are echoed back, not the internal System One labels. The published beta accepts string/boolean values; the toolkit also accepts numbers as a local extension.
- Duplicate question names return
400. - Images in
inputreturn501(multimodal_not_supported); the published beta supports inline images, but the current catalog has no vision model. variantis a local extension; standard clients omit it.- OpenAI SDK clients can list models (
client.models.list()) and parse the error envelope ({"error": {message, type, param, code}}).
Auth¶
Bearer auth is optional and disabled by default for local use:
dtk serve --model gliner2.5-multi --api-key "$DECISION_TOOLKIT_API_KEY"
- Every protected
/v1endpoint (models, catalog, info, decisions) requiresAuthorization: Bearer <key>when configured. /healthand/readystay public.- Missing/invalid keys return
401withWWW-Authenticate: Bearerand the standard error envelope. - Comparison is constant-time (
hmac.compare_digest).
Readiness and warmup¶
A model is not "ready" merely because it is configured:
/readyreturns503with{"ready": false, "detail": ...}until the default model has been warmed.dtk serve --warmupwarms it at startup.- The first successful decision also warms it.
--max-models Nkeeps at most N idle models loaded (LRU). Models in use are never evicted.
Limits and transport¶
- 1–128 questions (local limit; the TypeSafe schema has no maximum).
- 1–255 choice options; 1–10 System One score levels; OpenAI score levels require at least 2.
- 8 legacy files/images maximum (the current catalog supports 0); standard
assetsand images are rejected with501. - Request body limit (
--max-body-mb, default 16) returns413. - Per-decision timeout (
--timeout, default 120 s) returns504. - CORS origins are explicit (
--cors-origin, repeatable). - Every response carries
x-request-id(accepted from the client or generated). dtk serve --log-format jsonemits one JSON object per line;textis the default. State/question content is not logged.--log-levelcontrols the toolkit logger.
Error mapping¶
/v1/systemone request-validation failures follow the TypeSafe schema:
422 with {"detail": [...]}. /v1/decisions validation and toolkit domain
errors use the OpenAI envelope
{"error": {"message", "type", "param", "code"}}.
| Condition | System One | OpenAI Decisions |
|---|---|---|
| Request-schema validation | 422 + detail |
400 + error envelope |
| Toolkit question validation | 422 + detail |
400 + error envelope |
| Unknown model | 404 |
404 |
| Model not downloaded | 503 |
503 |
Unsupported box / assets / images |
501 |
501 |
| Truncation in strict mode | 400 |
400 |
| Body too large | 413 |
413 |
| Missing/invalid bearer | 401 |
401 |
| Timeout | 504 |
504 |
The CLI/REST never parse error strings to decide behavior; they use the stable
code.
Versioning and evolving standards¶
The standards are still moving. The projection is designed to change without touching the core or the adapters:
- Detect upstream change: diff the System One / OpenAI spec against the pinned module.
- Map impact: update only
contracts/<spec version>.pyand itsmapping.pyprojection. - Core impact check: if the new standard needs a capability the core does not have, add it to the core first; never implement it in the server.
- Conformance: run the reference-client and core-vs-HTTP equivalence
tests, update the compatibility matrix, and publish the version in
/v1/info. - Compatibility: keep the old projection while a deprecation window is
open; version endpoints or media types on a breaking change. Extensions
always live under
x_decision_toolkit.
Adding a new spec version means adding a new module, not editing the old one.
Compatibility matrix (current)¶
| Feature | System One (TypeSafe 0.2.0) | OpenAI Decisions beta |
|---|---|---|
choice / noul / score |
yes | choice / predicate / score |
box |
typed by core; rejected by current text models | no |
| Text state | yes | input string |
| JSON state | yes | message/string input |
| Images/assets | 501 for current models |
501 for current models |
| Usage | input_tokens + non-negative output_tokens |
input/output/total + details |
| Per-request model | standard field; optional local default | standard field; optional local default |
variant |
local extension | local extension |
| Validation errors | 422 + detail |
400 + envelope |
| Auth / CORS / limits | server-level | server-level |
Running¶
dtk serve --model gliner2.5-multi --warmup \
--api-key "$DECISION_TOOLKIT_API_KEY" \
--cors-origin http://localhost:3000 \
--log-format json --log-level INFO
Then point a System One client at http://localhost:8000 or an OpenAI client
at http://localhost:8000/v1.
Testing¶
- System One contract tests cover the published request/response fields, non-negative usage, the invalid request matrix and image/asset status codes.
- OpenAI contract tests use the official
openaiPython SDK formodels.list()and error-envelope parsing. - Equivalence tests send the same core decision through HTTP and compare answers, usage and metadata.
- OpenAPI tests assert the real Pydantic request/response/error schemas.
- Transport tests cover auth, request ids, 413, CORS, readiness and per-request model selection.
Out of scope for phase 2¶
- Metrics/Prometheus, rate limiting and load tests are implemented in phase 5 (see OPERATIONS.md).
- CLI batch/JSONL and custom-model workflows are implemented in phase 3 (see CLI.md).
- Pinned catalog hashes and source CI are implemented in phase 4; wheel, PyPI and distribution tagging are documented in RELEASE.md.