> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Decisions API (/v1/decisions, /v1/systemone)

> Evaluate text against typed yes/no, choice, and score questions on /v1/decisions and /v1/systemone through TrueFoundry AI Gateway.

Decision models answer typed questions about an input and return probabilities instead of generated text: a yes/no probability, a pick from a fixed list, or a score on a scale. The AI Gateway serves them on two routes that share one provider list, so any model whose type is **Decision** works on either route.

| Route | Alias | Request format | Use it when |
| - | - | - | - |
| `POST /v1/decisions` | `/decisions` | OpenAI Decisions: `input` plus a list of `predicate`, `choice`, and `score` questions | You write against [OpenAI's format](https://developers.openai.com/api/docs/guides/decisions) or need image input |
| `POST /v1/systemone` | `/systemone` | System One: `state` plus a map of `noul`, `choice`, and `score` questions | You already have TypeSafe Jev request bodies |

The route you call does **not** limit which provider you can use. OpenAI receives OpenAI-format bodies; every other provider receives System One bodies. Answers come back in the format of the route you called.

| Feature | Supported | Notes |
| - | - | - |
| Tracing | Yes | Same request logs and spans as other gateway APIs |
| Cost tracking | Yes | Decision models bill **input tokens** only; set output cost to `0` |
| Rate limits, budgets, access control | Yes | Same as any other model |
| Virtual models, load balancing, fallbacks | Yes | A virtual model can mix OpenAI and System One targets |
| Streaming | No | |
| Image input | OpenAI on `/v1/decisions` only | Other providers return `400` |

<Note>
  Add the model with **Model Types** set to **Decision**. A chat model cannot serve these routes. Custom Endpoint pass-through of TypeSafe (the [Jev](/docs/ai-gateway/jev) guide) is a different path — it does not use `/v1/systemone` on the gateway, and it has no cost tracking.
</Note>

## Provider capabilities

<Info>
  Legend:

  * **✅** Supported by provider and TrueFoundry
  * <Icon icon="circle-xmark" iconType="regular" color="red" /> Provided by provider, but not by TrueFoundry
  * <Icon icon="circle-minus" iconType="regular" /> Provider does not support this feature
</Info>

| Provider | Example model | Gateway route it speaks natively | Images |
| - | - | - | - |
| [OpenAI](/docs/ai-gateway/openai#decision-openai-decisions) | `gpt-6-luna` | `/v1/decisions` | Yes |
| [TypeSafe](/docs/ai-gateway/typesafe) (Jev, Cloudflare Clef) | `jev-1.13.0`, `typesafe/jev`, `@cf/cloudflare/clef` | `/v1/systemone` | Clef only |
| [OpenRouter](/docs/ai-gateway/openrouter#decision-system-one) | `typesafe/jev-1.13` | `/v1/systemone` | No |
| [Perplexity](/docs/ai-gateway/perplexity-ai#decision-system-one) | `pplx-decider-v1-27b` | `/v1/systemone` | No |
| [Databricks](/docs/ai-gateway/databricks-models#decision-system-one) | `databricks-openjev-qwen35-4b`, `system.ai.openjev-qwen35-4b` | `/v1/systemone` | No |
| [Microsoft Foundry](/docs/ai-gateway/microsoft-foundry#decision-system-one) | `Microsoft-Decision-1` | `/v1/systemone` | No |
| [Self-hosted vLLM](/docs/ai-gateway/self-hosted-models#decision-system-one) | a Qwen3 / Qwen3.5 checkpoint | `/v1/systemone` | No |

For every gateway endpoint and provider, see [Supported APIs](/docs/ai-gateway/intro-to-llm-gateway#supported-apis).

The AI Gateway translates between the two formats per target:

* Text parts of an OpenAI `input` are joined into the System One `state`. System One questions are named by their map keys when they go to OpenAI.
* A noul's `criteria` have no OpenAI field, so they are folded into the `instructions` OpenAI sees. Missing instructions on a System One question get a generic fallback such as "Is this true of the input?".
* `safety_identifier` is forwarded to OpenAI and dropped for other providers.
* `usage` is left in the provider's own shape so cost tracking still works.

## Add a decision model

Pick the provider, add the model with **Model Types** = **Decision**, then call either route with the TrueFoundry FQN `account-name/model-display-name`.

<Steps>
  <Step title="Add the provider account">
    Follow the provider page for credentials and base URL:

    * [TypeSafe](/docs/ai-gateway/typesafe) — TypeSafe API key; optional Base URL for Cloudflare Workers AI
    * [OpenAI](/docs/ai-gateway/openai)
    * [OpenRouter](/docs/ai-gateway/openrouter)
    * [Perplexity](/docs/ai-gateway/perplexity-ai)
    * [Databricks](/docs/ai-gateway/databricks-models)
    * [Microsoft Foundry](/docs/ai-gateway/microsoft-foundry)
    * [Self-hosted vLLM](/docs/ai-gateway/self-hosted-models)
  </Step>

  <Step title="Add the model with type Decision">
    Set **Model Types** to **Decision**. Pin a versioned **Model ID** where the provider offers one (`jev-1.13.0` rather than `jev-latest`) so confidence thresholds stay stable across releases.
  </Step>

  <Step title="Call either route">
    Send your TrueFoundry API key to the AI Gateway. The gateway injects the upstream credential. Set `GATEWAY_BASE_URL` without a trailing slash.
  </Step>
</Steps>

## `/v1/systemone`

The System One format. Send the content in `state` and named questions in `questions`. Answers come back keyed by those names.

Set `GATEWAY_BASE_URL` to your gateway base URL without a trailing slash and `TFY_API_KEY` to your TrueFoundry API key.

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os

  import requests

  gateway_base_url = os.environ["GATEWAY_BASE_URL"].rstrip("/")
  response = requests.post(
      f"{gateway_base_url}/v1/systemone",
      headers={"Authorization": f"Bearer {os.environ['TFY_API_KEY']}"},
      json={
          "model": "my-typesafe-account/jev",
          "state": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today.",
          "questions": {
              "is_refund_request": {
                  "type": "noul",
                  "instructions": "Is the customer asking for a refund?",
              },
              "urgency": {
                  "type": "choice",
                  "instructions": "How urgent is this?",
                  "criteria": {
                      "low": "can wait a week",
                      "high": "needs action today",
                  },
              },
              "frustration": {
                  "type": "score",
                  "instructions": "How frustrated is the customer?",
                  "criteria": ["calm", "mildly annoyed", "angry"],
              },
          },
      },
      timeout=60,
  )
  response.raise_for_status()
  print(response.json()["answers"]["is_refund_request"])
  ```

  ```bash cURL theme={"dark"}
  curl --fail-with-body -X POST "${GATEWAY_BASE_URL}/v1/systemone" \
    -H "Authorization: Bearer ${TFY_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "my-typesafe-account/jev",
      "state": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today.",
      "questions": {
        "is_refund_request": {
          "type": "noul",
          "instructions": "Is the customer asking for a refund?"
        },
        "urgency": {
          "type": "choice",
          "instructions": "How urgent is this?",
          "criteria": {
            "low": "can wait a week",
            "high": "needs action today"
          }
        },
        "frustration": {
          "type": "score",
          "instructions": "How frustrated is the customer?",
          "criteria": ["calm", "mildly annoyed", "angry"]
        }
      }
    }'
  ```
</CodeGroup>

Example response:

```json theme={"dark"}
{
  "model": "jev-1.13.0",
  "answers": {
    "is_refund_request": { "type": "noul", "noul": 0.99 },
    "urgency": {
      "type": "choice",
      "choice": "high",
      "confidence": 1.0,
      "probabilities": { "low": 0.0, "high": 1.0 }
    },
    "frustration": {
      "type": "score",
      "score": 0.95,
      "confidence": 0.86,
      "legend": { "0": "calm", "1": "mildly annoyed", "2": "angry" },
      "probabilities": { "0": 0.07, "1": 0.91, "2": 0.02 }
    }
  },
  "usage": { "input_tokens": 391, "output_tokens": 67 }
}
```

Changing `"model"` to an OpenAI decision FQN asks OpenAI instead and still returns System One answers keyed by question name.

| Field | Type | Description |
| - | - | - |
| `model` | string | TrueFoundry model FQN, `account-name/model-display-name` |
| `state` | string, object, or array | The content being judged — a support ticket, a document, or a transcript |
| `questions` | object | Named questions. Each key becomes a key in `answers` |

Every question has a `type` and an optional `instructions` field.

| `type` | `criteria` | Answer fields |
| - | - | - |
| `noul` | Optional object with `true` and `false` descriptions. A noul question needs `instructions` or `criteria`. | `noul`: probability that the answer is true, from 0 to 1 |
| `choice` | Required object mapping labels to descriptions | `choice`, `confidence`, `probabilities` per label |
| `score` | Required array of levels, lowest first | `score` (0 to levels − 1), `confidence`, `legend`, `probabilities` per level |

See the [TypeSafe API reference](https://api.typesafe.ai/docs) for the full System One schema.

## `/v1/decisions`

The OpenAI Decisions format. Send the content in `input` and a list of questions. Answers come back as a list in the same order, echoing each `name`.

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os

  import requests

  gateway_base_url = os.environ["GATEWAY_BASE_URL"].rstrip("/")
  response = requests.post(
      f"{gateway_base_url}/v1/decisions",
      headers={"Authorization": f"Bearer {os.environ['TFY_API_KEY']}"},
      json={
          "model": "openai-main/gpt-6-luna",
          "input": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today.",
          "questions": [
              {
                  "type": "predicate",
                  "name": "is_refund_request",
                  "instructions": "Is the customer asking for a refund?",
              },
              {
                  "type": "choice",
                  "name": "needs_human",
                  "instructions": "Should a human agent take over?",
                  "choices": [
                      {"value": True, "description": "billing dispute that needs a person"},
                      {"value": False},
                  ],
              },
              {
                  "type": "score",
                  "name": "frustration",
                  "instructions": "How frustrated is the customer?",
                  "levels": [
                      {"label": "calm"},
                      {"label": "mildly annoyed"},
                      {"label": "angry", "description": "uses hostile language"},
                  ],
              },
          ],
      },
      timeout=60,
  )
  response.raise_for_status()
  print(response.json()["answers"][0])
  ```

  ```bash cURL theme={"dark"}
  curl --fail-with-body -X POST "${GATEWAY_BASE_URL}/v1/decisions" \
    -H "Authorization: Bearer ${TFY_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "openai-main/gpt-6-luna",
      "input": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today.",
      "questions": [
        {
          "type": "predicate",
          "name": "is_refund_request",
          "instructions": "Is the customer asking for a refund?"
        },
        {
          "type": "choice",
          "name": "needs_human",
          "instructions": "Should a human agent take over?",
          "choices": [
            {"value": true, "description": "billing dispute that needs a person"},
            {"value": false}
          ]
        },
        {
          "type": "score",
          "name": "frustration",
          "instructions": "How frustrated is the customer?",
          "levels": [
            {"label": "calm"},
            {"label": "mildly annoyed"},
            {"label": "angry", "description": "uses hostile language"}
          ]
        }
      ]
    }'
  ```
</CodeGroup>

Changing `"model"` to a TypeSafe Jev FQN sends the same questions to TypeSafe and still returns OpenAI-shaped answers.

| Field | Type | Description |
| - | - | - |
| `model` | string | TrueFoundry model FQN, `account-name/model-display-name` |
| `input` | string or array | A string, or user messages whose `content` is a string or a list of `input_text` and `input_image` parts |
| `questions` | array | Questions to evaluate. Answers come back in the same order |
| `safety_identifier` | string, optional | A stable ID for your end user. Sent to OpenAI and dropped for other providers |

Every question takes `instructions` and an optional `name` that is echoed in its answer. An unnamed question is keyed `question_<index>` when it is translated to System One.

| `type` | Extra fields | Answer fields |
| - | - | - |
| `predicate` | None | `probability` that the answer is yes |
| `choice` | `choices`: entries of `{"value": string or boolean, "description": optional}` | `choice`, `probabilities` per value, `confidence` |
| `score` | `levels`: entries of `{"label": ..., "description": optional}`, lowest first | `score` (the probability-weighted level index), `probabilities` per level, `confidence` |

A question the OpenAI model will not answer comes back as `{"type": "refusal", "name": ...}`.

### Image input

`input_image` parts only work on OpenAI models, on `/v1/decisions`. `image_url` must be an inline `data:` URL — hosted URLs and file ids are not supported.

```bash theme={"dark"}
curl --fail-with-body -X POST "${GATEWAY_BASE_URL}/v1/decisions" \
  -H "Authorization: Bearer ${TFY_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai-main/gpt-6-luna",
    "input": [
      {
        "role": "user",
        "content": [
          {"type": "input_text", "text": "Here is the swatch the customer uploaded."},
          {"type": "input_image", "image_url": "data:image/png;base64,<BASE64_PNG>"}
        ]
      }
    ],
    "questions": [
      {"type": "predicate", "name": "is_red", "instructions": "Is the swatch mostly red?"}
    ]
  }'
```

The same body with a TypeSafe, OpenRouter, Perplexity, Databricks, Microsoft Foundry, or vLLM model is rejected **before** that provider is called:

```json theme={"dark"}
{
  "error": {
    "message": "No model available for this request can represent this decision. input_image parts are not supported because System One providers accept text input only.",
    "type": "invalid_request_error"
  }
}
```

Cloudflare Clef, added through a TypeSafe account pointed at Workers AI, accepts images on the System One `images` field instead — see [TypeSafe](/docs/ai-gateway/typesafe#cloudflare-workers-ai).

## Limits

These requests are rejected with a `400` before any provider is called:

| Situation | What happens |
| - | - |
| Image parts on a System One provider | `input_image parts are not supported because System One providers accept text input only` |
| Two OpenAI questions with the same `name` | The names would collide into one System One answer |
| A choice with both `true` and `"true"` | System One keys choices by their text |
| A choice with one label, or a score with one level, sent to OpenAI | OpenAI requires at least two choices or levels |
| vLLM asked a `noul` or `score` question | vLLM serves `choice` questions only; the upstream `400` is forwarded |

OpenAI requires `instructions` on every question. When a System One question without `instructions` goes to OpenAI, the AI Gateway fills in a generic one.

Self-hosted vLLM answers only `choice` questions, and needs a vLLM build that includes [vllm#59299](https://github.com/vllm-project/vllm/pull/59299) (after v0.31.0).

## Virtual models

A virtual model whose model types include `decision` can load-balance or fail over across decision targets, including a mix of OpenAI and System One providers. The AI Gateway translates each attempt into that target's dialect and translates the successful answer back into the dialect of the route you called.

If every target cannot represent the request — for example every remaining target is System One and the body contains `input_image` — the gateway returns `400` with `No model available for this request can represent this decision`.

Auto Routing can also use a TypeSafe Jev model as the **classifier** that picks `simple` / `medium` / `complex` for a chat virtual model. That is a routing setting, not a call you make to `/v1/systemone`. See [Auto Routing](/docs/ai-gateway/auto-routing).

## Custom Endpoints vs these routes

The [Jev Custom Endpoints](/docs/ai-gateway/jev) guide forwards raw HTTP to `https://api.typesafe.ai`. You keep TypeSafe credentials on the gateway, but the path is `/proxy-api/.../v1/systemone`, cost tracking is unavailable, and you cannot switch providers by changing `model`.

`/v1/decisions` and `/v1/systemone` use models in your catalog, so you get access control, tracing, cost tracking, virtual models, and the ability to change provider without changing the request body. Prefer the native [TypeSafe](/docs/ai-gateway/typesafe) integration unless you specifically need a transparent proxy.

## Next steps

* [TypeSafe](/docs/ai-gateway/typesafe) — add Jev, including via Cloudflare Workers AI.
* [OpenRouter](/docs/ai-gateway/openrouter#decision-system-one) — call Jev with an OpenRouter API key.
* [Auto Routing](/docs/ai-gateway/auto-routing) — let Jev pick the chat-model tier for each request.
* [TypeSafe API reference](https://api.typesafe.ai/docs) — System One question types.
* [OpenAI Decisions](https://developers.openai.com/api/docs/guides/decisions) — OpenAI request and response schema.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.