# Decision models

> Run decision policies, rerank, classifier guardrails, the route classifier and evals on the decision model of your choice, on your own account with its provider.

Source: https://infro.io/docs/features/decision-models

> **Note:** INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See [Early access](https://infro.io/early-access).

A decision model answers typed questions — the probability that a statement is true, one option of a closed set, a level on a rubric — and generates no text. TypeSafe's Jev was the first; OpenAI, Cloudflare, Perplexity, Inception, Liquid AI, Upstage and Microsoft now ship one. INFRO runs every [decision feature](https://infro.io/docs/features/decisions) on whichever of them you connect, translating each provider's wire into one shape, so a policy, a guardrail check or a route classifier names a model and nothing else changes.

> **Note:** Every call runs on **your own account with that provider**, which bills it. INFRO charges nothing per decision; the estimated provider cost of each call is on its row in the request log, separate from your INFRO subscription.

## The models

| Model | Connect | Images |
| --- | --- | --- |
| `typesafe/jev-1.13` — Jev 1.13 | TypeSafe | Text and JSON only |
| `openai/gpt-6-luna-decisions` — GPT-6 Luna (Decisions) | OpenAI (the Decisions API) | Data URLs |
| `cloudflare/clef` — Clef | Cloudflare Workers AI (API token and account ID) | Data URLs, up to 4 |
| `cloudflare/clef-flash` — Clef Flash | Cloudflare Workers AI | Data URLs, up to 4 |
| `cloudflare/clef-omni` — Clef Omni | Cloudflare Workers AI | Data URLs, up to 4; audio and video are not offered yet |
| `perplexity/pplx-decider-v1.1-27b` — pplx-decider v1.1 | Perplexity | Data URLs |
| `inception/mercury-decide` — Mercury Decide | Inception | Text and JSON only |
| `liquid/d1` — d1 | Liquid AI | Data URLs, up to 8 |
| `upstage/solar-decide` — Solar Decide | Upstage (in beta at Upstage) | Text and JSON only |
| `upstage/solar-decide-flash` — Solar Decide Flash | OpenRouter only | Text and JSON only |
| `microsoft/microsoft-decision-1` — Microsoft-Decision-1 | Azure OpenAI (a Foundry deployment, mapped to its deployment name) | Text and JSON only |

An [OpenRouter](https://infro.io/docs/providers) connection reaches every one of them too, on OpenRouter's own System One endpoint. Connect both a provider and OpenRouter, or two providers' models in a [route](https://infro.io/docs/routes), and a decision fails over between them like any other request.

Name a model by its slug in a decision policy, a guardrail's classifier checks, a route's prompt classifier, `POST /v1/rerank`, `POST /v1/systemone` or `POST /v1/decisions`. On a decision endpoint, `gpt-6-luna` and `openai/gpt-6-luna` mean GPT-6 Luna's decisions; on chat completions they mean the chat model. A decision model named on chat completions is refused with `400 decision_model`.

## POST /v1/decisions in OpenAI's dialect

OpenAI's SDK (`client.decisions.create`) works against INFRO with the base URL and the key changed. The request runs on your own connections with every INFRO policy applied — key scopes, budgets, routes and failover, the request log — and the model need not be OpenAI's: any decision model answers in OpenAI's shape.

```python
import os
from openai import OpenAI

# The INFRO gateway key, not the OpenAI key: that stays in your INFRO connection.
client = OpenAI(base_url="https://api.infro.io/v1", api_key=os.environ["INFRO_API_KEY"])

decision = client.decisions.create(
    model="gpt-6-luna",  # or any decision model: "cloudflare/clef-flash", "typesafe/jev-1.13"
    input="I was charged twice for my order.",
    questions=[
        {
            "type": "choice",
            "name": "department",
            "instructions": "Which department should handle this complaint?",
            "choices": [
                {"value": "billing", "description": "Payments, invoices, and refunds."},
                {"value": "technical", "description": "Problems using the product."},
                {"value": "other", "description": "Requests outside these categories."},
            ],
        }
    ],
)
print(decision.answers[0])
```

```json
{
  "id": "req_…",
  "object": "decision",
  "model": "gpt-6-luna",
  "answers": [
    {
      "type": "choice",
      "name": "department",
      "choice": "billing",
      "probabilities": [
        { "value": "billing", "probability": 0.95 },
        { "value": "technical", "probability": 0.03 },
        { "value": "other", "probability": 0.02 }
      ],
      "confidence": 0.93
    }
  ],
  "usage": {
    "input_tokens": 61,
    "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
    "output_tokens": 0,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 61,
    "cost": 0.0000061,
    "price_source": "list"
  },
  "route": "primary",
  "route_name": null,
  "provider": "openai",
  "connection": "conn_…"
}
```

- `input` is a string, or user messages of `input_text` and `input_image` parts (at most 128 images). An image reaches only a model that sees images; a request whose model reads text only moves to its next fallback, and with none left is refused with `400 images_unsupported`. Every decision model takes images only as inline data URLs: Cloudflare, Perplexity and Liquid AI are refused a link with `400 image_url_unsupported` before any call, and OpenAI's Decisions API refuses one with its own `400`.
- `questions` are `predicate`, `choice` and `score`, as OpenAI documents them. On a System One model a predicate is a yes/no question, a choice's options are keyed by their text (a boolean value comes back as the boolean), and a score's level reads `label: description`.
- A question no model answered readably comes back as OpenAI's `{"type": "refusal"}`; an unnamed question is answered with `name: null`.
- `safety_identifier` is passed to OpenAI and to no other provider.
- Responses add `id`, `usage.cost`, `usage.price_source`, `route`, `route_name`, `provider` and `connection` to OpenAI's fields.
- `POST /v1/decisions/{policy}` is a saved [decision policy](https://infro.io/docs/api/decisions): a different route under the same path.

## Images on POST /v1/systemone

`POST /v1/systemone` takes TypeSafe's body and, for the models that see images, a top-level `images` array — data URLs, or `{content_type, base64}` — the extension Cloudflare and Liquid AI add to it. Each provider receives the images in its own form.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `decision_model` | A decision model was named on chat completions or another endpoint that is not a decision endpoint. |
| 400 | `images_unsupported` | The request carries an image and no model it could reach sees images. |
| 400 | `image_url_unsupported` | The model takes images only inline, and the request linked one. |
| 400 | `invalid_request` | The provider rejected the question set; the message carries its words. Not retried on another key: every key of a provider validates alike. |
| 503 | `no_provider_connected` | No connection serves the model. Connect its provider, or OpenRouter. |
