HTTP API

Decidealot runs configured local Laya, Von, and CLM providers or TypeSafe’s hosted Jev through one TypeSafe-compatible HTTP contract. Send the state to judge and define the allowed answer shape. Decidealot returns typed results with probabilities. The same operations are available as MCP tools; see MCP.

Base URL and startup

The examples use a local CPU container at http://127.0.0.1:8080. On a fresh model directory, Decidealot downloads and verifies every enabled local bundle before it starts listening, so /health does not answer at all until then. The default configuration enables Laya and Von. CLM is enabled when its required embeddings URL is configured. That first startup can take minutes. It prepares files only. Neither model nor Torch stays loaded until a POST /v1/systemone request selects a local model. Jev enables when a TypeSafe key is configured and needs no local bundle.

Every response includes X-Request-Id. Send a UUID or ULID in that header when you need to correlate logs and calls. Decidealot creates a UUID when it is missing or invalid.

base_url=http://127.0.0.1:8080
curl --fail --show-error "$base_url/health"
{
  "status": "ok",
  "providers": ["laya", "von"]
}

providers lists only configured providers. A CLM-only deployment returns {"status":"ok","providers":["clm"]}. A Jev-only deployment returns {"status":"ok","providers":["jev"]}. /health never needs authentication. It returns 503 with PROVIDER_UNAVAILABLE and local providers are not ready while the service is shutting down.

Authentication

Authentication is off unless the container has DECIDEALOT_API_KEY. When it is set, every public HTTP API or MCP request except /health needs this header:

auth_header="Authorization: Bearer $DECIDEALOT_API_KEY"
curl --fail --show-error "$base_url/v1/models" --header "$auth_header"

Missing or wrong credentials return 401:

{
  "code": "UNAUTHORIZED",
  "message": "a valid bearer token is required",
  "details": {}
}

List selectable models

GET /v1/models lists every accepted model value. Each POST /v1/systemone call names one selector from this catalog.

curl --fail --show-error "$base_url/v1/models" --header "$auth_header"
{
  "models": [
    {
      "name": "laya",
      "description": "Laya System One decisions with automatic checkpoint routing.",
      "release_date": "2026-09-23"
    },
    {
      "name": "laya-auto",
      "description": "Alias for laya with automatic checkpoint routing.",
      "release_date": "2026-09-23"
    },
    {
      "name": "laya-latest",
      "description": "Alias for the current Laya release.",
      "release_date": "2026-09-23"
    },
    {
      "name": "laya-english",
      "description": "Laya English checkpoint for English Latin-script content.",
      "release_date": "2026-09-23"
    },
    {
      "name": "laya-multilingual",
      "description": "Laya multilingual checkpoint for other languages and scripts.",
      "release_date": "2026-09-23"
    },
    {
      "name": "laya-typed-decisions",
      "description": "Laya checkpoint tuned for structured workflow decisions.",
      "release_date": "2026-09-23"
    },
    {
      "name": "von",
      "description": "Von System One decisions served by the local Von 1.1 model.",
      "release_date": "2026-09-22"
    },
    {
      "name": "von-latest",
      "description": "Alias for the current Von release.",
      "release_date": "2026-09-22"
    },
    {
      "name": "von-1.1",
      "description": "Von 1.1 System One decision model.",
      "release_date": "2026-09-22"
    },
    {
      "name": "von-1.1.0",
      "description": "Alias for the Von 1.1 point release.",
      "release_date": "2026-09-22"
    }
  ]
}

The default catalog above contains Laya and Von. When CLM is enabled, the response also contains clm, clm-latest, clm-0.1, and clm-0.1-8b. Those selectors run the local Contrastive-LM/CLM-v0.1-8B projection head. CLM sends rendered decision state and criterion text to the configured OpenAI-compatible embeddings endpoint, which must return Qwen3-8B last-token vectors with exactly 4096 float values for each input. When Jev is enabled, Decidealot adds the models and aliases returned by TypeSafe’s authenticated GET /v1/models. It caches that catalog for 60 seconds and refreshes it on the next model-listing or hosted-model request after expiry. The listed hosted names are passed through exactly; Decidealot does not invent a jev alias. Local selectors win if TypeSafe ever returns a colliding name. If TypeSafe’s catalog is unavailable or malformed, model listing and hosted requests return a safe 503; local decisions still work.

Make decisions

POST /v1/systemone always needs these fields:

Field Type Meaning
model string One exact selector from GET /v1/models.
state string, object, or array The raw thing to classify, score, or judge.
questions object One or more named choice, score, or noul questions.
config optional object Provider-specific request settings. Omitted means native defaults.

Question names are yours. The response repeats them under answers. instructions is optional in the wire schema, but supply it to state the question and decision rules explicitly. This matters especially for noul: a question name alone is not the proposition to evaluate. Instructions, state, and criterion values can be strings, JSON objects, or JSON arrays. Laya and Von receive nested instructions and criterion values as JSON text without changing labels, score order, or state. CLM preserves their structure and renders it using upstream’s context-then-question layout. Jev receives the validated JSON structure with the exact hosted model name you selected.

Request configuration

config is flat because model already selects its schema. CLM selectors accept {"temperature":0.8}. Temperature defaults to 1 and must be a finite number greater than 0 and at most 100. It divides the scaled CLM logits before softmax. Lower temperatures sharpen probabilities, not accuracy. Strings, booleans, null, non-object configs, unknown keys, and values outside this range return 422. Laya, Von, and hosted Jev accept only omitted or empty config; Decidealot strips the empty object before sending TypeSafe its official three-field request.

{
  "model": "clm",
  "config": {"temperature": 0.8},
  "state": "The customer requests a refund for a duplicate payment.",
  "questions": {
    "queue": {
      "type": "choice",
      "instructions": "Route payment or refund questions to billing; software failures to technical.",
      "criteria": {"billing": "Payment or refund question", "technical": "Software failure"}
    }
  }
}

The response remains model, answers, and usage. The MCP system_one tool accepts the same optional config. Each request in HTTP batch or MCP system_one_batch has its own config. All batch configs are checked before executing any decision; invalid settings report body.requests.<index>.config and no item executes. Provider or encoder failures during execution still fail the batch without partial results and may happen after earlier items completed.

CLM bounds each rendered state plus instructions and each candidate by DECIDEALOT_CLM_MAX_TEXT_BYTES (default 8192 UTF-8 bytes) before contacting its encoder. Over-limit inputs return 422; Decidealot does not request truncation. Bytes are not tokens. The encoder’s context limit and truncation behavior remain deployment settings. CLM caches candidate vectors only, with configurable LRU capacity and TTL. State vectors are always requested again. Cache hits reduce usage.input_tokens; it reports encoder usage for actual calls, not the hypothetical uncached workload.

The response shape always contains model, answers, and usage. The numeric values below are examples. A real model call chooses its own answer and probabilities.

Choice

Use choice when the model must select one named result. criteria is an object. Its keys are the only valid choice labels.

curl --fail --show-error "$base_url/v1/systemone" \
  --header 'Content-Type: application/json' \
  --header "$auth_header" \
  --data '{
    "model": "laya-typed-decisions",
    "state": {
      "operation": "delete",
      "resource": "protected-record",
      "reversible": false
    },
    "questions": {
      "handling": {
        "type": "choice",
        "instructions": "Select the required handling.",
        "criteria": {
          "allow": "The operation is reversible and does not affect protected data.",
          "require_review": "The operation is irreversible or affects protected data.",
          "deny": "The operation is prohibited."
        }
      }
    }
  }'
{
  "model": "laya-typed-decisions",
  "answers": {
    "handling": {
      "type": "choice",
      "choice": "require_review",
      "confidence": 0.94,
      "probabilities": {
        "allow": 0.02,
        "require_review": 0.94,
        "deny": 0.04
      }
    }
  },
  "usage": {
    "input_tokens": 91,
    "output_tokens": 0
  }
}

Score

Use score for an ordered rubric. criteria is a nonempty array. Array position is the score, starting at 0. The answer can fall between those positions because it is an expected score.

curl --fail --show-error "$base_url/v1/systemone" \
  --header 'Content-Type: application/json' \
  --header "$auth_header" \
  --data '{
    "model": "von-1.1",
    "state": "A customer reports repeated failures after following the documented setup.",
    "questions": {
      "priority": {
        "type": "score",
        "instructions": "Score the response priority.",
        "criteria": [
          "Routine, answer in the normal queue.",
          "Important, answer soon.",
          "Urgent, interrupt the on-call queue."
        ]
      }
    }
  }'
{
  "model": "von-1.1",
  "answers": {
    "priority": {
      "type": "score",
      "score": 1.63,
      "confidence": 0.81,
      "legend": {
        "0": "Routine, answer in the normal queue.",
        "1": "Important, answer soon.",
        "2": "Urgent, interrupt the on-call queue."
      },
      "probabilities": {
        "0": 0.08,
        "1": 0.21,
        "2": 0.71
      }
    }
  },
  "usage": {
    "input_tokens": 62,
    "output_tokens": 0
  }
}

Noul

Use noul for a true or false judgment. The response contains the probability that the statement is true.

curl --fail --show-error "$base_url/v1/systemone" \
  --header 'Content-Type: application/json' \
  --header "$auth_header" \
  --data '{
    "model": "laya",
    "state": [
      "The requested change removes an audit record.",
      "The requester has no documented exception."
    ],
    "questions": {
      "needs_review": {
        "type": "noul",
        "instructions": "Is human review required before the change runs?",
        "criteria": {
          "true": "Review is required before the operation.",
          "false": "The operation may run without human review."
        }
      }
    }
  }'
{
  "model": "laya",
  "answers": {
    "needs_review": {
      "type": "noul",
      "noul": 0.97
    }
  },
  "usage": {
    "input_tokens": 58,
    "output_tokens": 0
  }
}

More than one question

Put as many questions as you need into one questions object. Each may use a different response type and each answer keeps the matching type. This request sends an object as state, a choice, a score, and a noul together:

{
  "model": "von",
  "state": {
    "summary": "A request changes access for an account that holds protected records.",
    "requestedBy": "automated-worker",
    "hasApproval": false
  },
  "questions": {
    "route": {
      "type": "choice",
      "criteria": {
        "standard": "Handle through the normal workflow.",
        "review": "Send to a human reviewer."
      }
    },
    "urgency": {
      "type": "score",
      "criteria": ["routine", "important", "urgent"]
    },
    "can_run": {
      "type": "noul",
      "criteria": {
        "true": "The request may run now.",
        "false": "The request must wait."
      }
    }
  }
}

Use the probabilities

Decidealot returns the model result and its probabilities. Your caller applies the policy. For example, it can allow an allow choice with a probability of at least 0.95, queue the rest for review, and store the full response beside its action record.

choice.confidence and score.confidence are model confidence values. choice.probabilities maps each criterion label to its probability. score.probabilities maps score positions to their probabilities. noul is the probability of true. Choose thresholds from the cost of being wrong in your workflow.

Batch independent decisions

POST /v1/systemone/batch takes requests, a nonempty list of complete System One request objects. Each item can choose a different model, state, and set of questions. The same model can appear more than once. Requests to the same underlying model run in input order. Different models can overlap when their execution policy permits it. Decidealot returns one normal System One result per item, in input order even if another model finishes first. By default, item count has no separate cap beyond DECIDEALOT_MAX_REQUEST_BYTES; set DECIDEALOT_MAX_BATCH_REQUESTS to a positive number to reject larger batches with 422. Set DECIDEALOT_MAX_BATCH_CONCURRENCY to a positive number to cap simultaneous provider calls made by batches without limiting batch length. This is a Decidealot extension, not a TypeSafe endpoint. The single-model endpoint keeps its existing response shape.

curl --fail --show-error "$base_url/v1/systemone/batch" \
  --header 'Content-Type: application/json' \
  --header "$auth_header" \
  --data '{
    "requests": [
      {
        "model": "laya",
        "state": "A request would delete an audit record.",
        "questions": {"review": {"type": "noul", "instructions": "Is human review needed?"}}
      },
      {
        "model": "von",
        "state": "A support request arrived with no account identifier.",
        "questions": {"queue": {"type": "choice", "criteria": {"standard": "Normal queue", "review": "Needs manual routing"}}}
      }
    ]
  }'
{
  "results": [
    {"model": "laya", "answers": {"review": {"type": "noul", "noul": 0.92}}, "usage": {"input_tokens": 21, "output_tokens": 0}},
    {"model": "von-1.1", "answers": {"queue": {"type": "choice", "choice": "review", "confidence": 0.8, "probabilities": {"standard": 0.2, "review": 0.8}}}, "usage": {"input_tokens": 23, "output_tokens": 0}}
  ]
}

The numbers above illustrate the response shape, not a guaranteed model decision. Each model names the model that answered, which can differ from its requested alias. With one resident slot, Laya and Von wait and switch models rather than run together. CPU deployments with two slots may overlap them; CUDA inference remains serial. CLM and its Qwen encoder share the local serial lane by default. Set DECIDEALOT_CLM_PARALLEL_WITH_LOCAL_MODELS=true only when it is safe for CLM to overlap other local models, such as when Qwen runs on another machine. Hosted Jev can overlap local work. If any item fails, the batch returns that item’s validation or unavailable error; it does not return partial results. Later requests for the same model may not be forwarded after one fails. The MCP system_one_batch tool takes the identical { "requests": [...] } arguments and returns the identical structured result.

CLM requests

CLM uses the same request and response contract as Laya and Von. Set model to a CLM selector returned by GET /v1/models. It does not accept an endpoint URL in a request. The operator configures one fixed embeddings endpoint at startup, and every CLM request goes only there.

{
  "model": "clm",
  "state": {
    "operation": "delete",
    "reversible": false
  },
  "questions": {
    "needs_review": {
      "type": "noul",
      "instructions": "Does this action need human review before it runs?"
    }
  }
}

CLM returns the normal TypeSafe answer shape. Its usage.input_tokens value is the usage.prompt_tokens reported by the configured embeddings endpoint. It has no output tokens.

Hosted Jev

Set DECIDEALOT_TYPESAFE_API_KEY in the server’s private environment. It enables Jev automatically unless DECIDEALOT_JEV_ENABLED=false. This key is for outbound TypeSafe calls; DECIDEALOT_API_KEY independently protects callers of Decidealot. The entire decision state and questions leave your host for TypeSafe. No request can choose another upstream URL.

{
  "model": "jev-latest",
  "state": {"message": "Please review the attached invoice."},
  "questions": {
    "queue": {
      "type": "choice",
      "instructions": {"task": "Select the appropriate queue."},
      "criteria": {"billing": "Invoices and payments", "general": "Other requests"}
    },
    "urgent": {"type": "noul", "instructions": "Does this need an immediate response?"}
  }
}

The response has the same model, answers, and usage fields as local decisions. TypeSafe reports which model answered, which can differ from the selected alias. Choice and Noul probabilities come from TypeSafe; Decidealot does not turn them into an allow or deny action. Hosted Jev does not load, unload, or switch a local provider. TypeSafe’s published OpenAPI contract defines the upstream question and answer shapes.

Unload loaded runtimes

Local providers also unload on their own after DECIDEALOT_PROVIDER_IDLE_UNLOAD_SECONDS, and the least recently used idle one makes room when the resident limit is reached; Models covers both. Hosted Jev has no local runtime to unload.

POST /v1/models/unload is the one public unload operation. It is idempotent and releases every loaded local provider. If any local provider has an active decision request, it returns 409 and releases none. A Jev request does not block local unload.

curl --fail --show-error --request POST "$base_url/v1/models/unload" \
  --header "$auth_header"
{
  "status": "unloaded",
  "providers": [
    {
      "name": "laya",
      "wasLoaded": true
    },
    {
      "name": "von",
      "wasLoaded": false
    }
  ]
}

Errors

Malformed System One bodies and unsupported model selectors return the TypeSafe-style 422 validation envelope before a provider receives a request.

curl --show-error "$base_url/v1/systemone" \
  --header 'Content-Type: application/json' \
  --header "$auth_header" \
  --data '{"model":"unknown-model","state":"anything","questions":{"answer":{"type":"noul"}}}'
{
  "detail": [
    {
      "loc": ["body", "model"],
      "msg": "Value error, unknown model 'unknown-model'; use a name returned by GET /v1/models",
      "type": "value_error",
      "input": "unknown-model"
    }
  ]
}

Failures outside that input contract use Decidealot’s envelope:

Status Code When it happens
401 UNAUTHORIZED Authentication is configured but the Bearer token is absent or wrong.
409 PROVIDER_BUSY An unload was requested while a provider is serving a decision.
413 REQUEST_TOO_LARGE The JSON body exceeds DECIDEALOT_MAX_REQUEST_BYTES.
503 PROVIDER_UNAVAILABLE The service is not ready, a local provider cannot start or stops before it is healthy, a provider times out or answers with a server error, TypeSafe rejects the upstream key, the hosted catalog is unavailable, or a provider gives an invalid response.
{
  "code": "PROVIDER_UNAVAILABLE",
  "message": "the selected provider is unavailable",
  "details": {}
}

A failed startup preparation never reaches this table: the process exits instead. Troubleshooting lists each message and its fix.

The model catalog restricts API callers to fixed configured providers. Each decision request supplies a model alias, state, and questions; it cannot supply a provider URL or upstream key.