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

# Decisions API

> Ask a decision model typed questions about an input and get probabilities back.

```bash cURL theme={null}
curl https://gateway.bud.studio/v1/decisions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "my-decision-deployment",
    "input": "Customer: I was charged twice for my order #1234 and want a refund now!",
    "questions": [
      {
        "name": "is_refund_request",
        "type": "predicate",
        "instructions": "Is the customer asking for a refund?"
      },
      {
        "name": "team",
        "type": "choice",
        "instructions": "Which team should handle this?",
        "choices": [
          { "value": "billing" },
          { "value": "shipping" },
          { "value": "technical", "description": "bugs and outages" }
        ]
      },
      {
        "name": "urgency",
        "type": "score",
        "instructions": "How urgent is this?",
        "levels": [{ "label": "low" }, { "label": "medium" }, { "label": "high" }]
      }
    ]
  }'
```

```python Python theme={null}
# openai>=3.26.0 has client.decisions
from openai import OpenAI

client = OpenAI(base_url="https://gateway.bud.studio/v1", api_key="YOUR_API_KEY")

decision = client.decisions.create(
    model="my-decision-deployment",
    input="Customer: I was charged twice for my order #1234 and want a refund now!",
    questions=[
        {"name": "is_refund_request", "type": "predicate",
         "instructions": "Is the customer asking for a refund?"},
        {"name": "team", "type": "choice", "instructions": "Which team should handle this?",
         "choices": [{"value": "billing"}, {"value": "shipping"},
                     {"value": "technical", "description": "bugs and outages"}]},
        {"name": "urgency", "type": "score", "instructions": "How urgent is this?",
         "levels": [{"label": "low"}, {"label": "medium"}, {"label": "high"}]},
    ],
)

for answer in decision.answers:  # one per question, in question order
    print(answer)
print(decision.usage)
```

```javascript Node theme={null}
// openai >= 7.30.0 has client.decisions
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://gateway.bud.studio/v1",
  apiKey: "YOUR_API_KEY",
});

const decision = await client.decisions.create({
  model: "my-decision-deployment",
  input: "Customer: I was charged twice for my order #1234 and want a refund now!",
  questions: [
    { name: "is_refund_request", type: "predicate", instructions: "Is the customer asking for a refund?" },
    {
      name: "team",
      type: "choice",
      instructions: "Which team should handle this?",
      choices: [{ value: "billing" }, { value: "shipping" }, { value: "technical", description: "bugs and outages" }],
    },
    {
      name: "urgency",
      type: "score",
      instructions: "How urgent is this?",
      levels: [{ label: "low" }, { label: "medium" }, { label: "high" }],
    },
  ],
});

for (const answer of decision.answers) console.log(answer);
console.log(decision.usage);
```

```json 200 theme={null}
{
  "model": "my-decision-deployment",
  "answers": [
    { "type": "predicate", "name": "is_refund_request", "probability": 1.0 },
    {
      "type": "choice",
      "name": "team",
      "choice": "billing",
      "confidence": 1.0,
      "probabilities": [
        { "value": "billing", "probability": 1.0 },
        { "value": "shipping", "probability": 0.0 },
        { "value": "technical", "probability": 0.0 }
      ]
    },
    {
      "type": "score",
      "name": "urgency",
      "score": 1.83,
      "confidence": 0.75,
      "probabilities": [
        { "value": 0, "label": "low", "probability": 0.01 },
        { "value": 1, "label": "medium", "probability": 0.15 },
        { "value": 2, "label": "high", "probability": 0.84 }
      ]
    }
  ],
  "usage": {
    "input_tokens": 547,
    "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
    "output_tokens": 0,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 547
  }
}
```

```json 200 (a refusal) theme={null}
{
  "model": "my-decision-deployment",
  "answers": [
    { "type": "refusal", "name": "steps_ok" },
    { "type": "predicate", "name": "is_harmful", "probability": 1.0 }
  ],
  "usage": {
    "input_tokens": 167,
    "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
    "output_tokens": 0,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 167
  }
}
```

A decision model answers typed questions about an input and returns probabilities over a closed set
of answers instead of generating text:

* **predicate**: the probability that a statement holds;
* **choice**: the chosen option, with the probability of every option;
* **score**: a position on an ordered scale, with the probability of every level.

The request and response follow OpenAI's Decisions API, so the OpenAI SDKs (openai-python 3.26.0 or
later, openai-node 7.30.0 or later) work by changing only the base URL. The same route serves every
decision model on Bud, whichever provider answers it: Bud translates the request for providers that
speak the System One format. To send System One requests yourself, use
[`POST /v1/systemone`](https://docs.budecosystem.com/api-sdk/decisions/system-one).

`model` is the name of your deployment. A deployment serves this route when its model lists the
Decisions API, which you can check with
[`GET /v1/models?capability=decisions`](https://docs.budecosystem.com/api-sdk/models/list-models).

## Decision models

| Provider | Models | Notes |
| - | - | - |
| OpenAI | `gpt-6-luna` | Also serves chat and responses. `gpt-5.6-luna` is not a decision model. |
| Azure OpenAI | a `gpt-6-luna` deployment | Same request and response as OpenAI. |
| TypeSafe | `jev-latest`, `jev-1.14.0`, `jev-1.13.0` | Decisions only; System One provider. See [TypeSafe](#typesafe). |
| Embedding Studio | `laya` | Decisions only; priced per request. See [Embedding Studio](#embedding-studio). |
| Any System One server | your model | Add it with the **System One compatible** provider and the server's decisions URL. |

> [!NOTE]
>
> A `gpt-6-luna` deployment created before your Bud release added the Decisions API does not serve
> `/v1/decisions` until it is redeployed. Deploying again adds the route; existing deployments are not
> widened automatically.

## Headers

| Parameter | Type | Required | Description |
| - | - | - | - |
| Authorization | string | Yes | Bearer authentication header |
| Content-Type | string | Yes | `application/json` |

## Body

| Parameter | Type | Required | Description |
| - | - | - | - |
| model | string | Yes | Name of a deployment that serves the Decisions API. |
| input | string or array | Yes | What to decide about: a non-empty string, or one or more input items (see [Input](#input)). |
| questions | array | Yes | 1 to 400 questions (see [Questions](#questions)). Answers come back in the same order. |
| prompt\_cache\_key | string | No | Up to 64 characters. Forwarded to OpenAI and Azure, scoped to your project so that it cannot share a cache with another tenant. No effect on System One providers. |
| prompt\_cache\_options | object | No | `mode` (`implicit` or `explicit`) and `ttl` (`30m`). Forwarded to OpenAI and Azure; no effect on System One providers. |
| safety\_identifier | string | No | Up to 128 characters. Forwarded to OpenAI and Azure only. |
| stream | — | No | Not supported: any truthy value is refused with `400` `stream_not_supported`. |

Other top-level fields are forwarded to OpenAI and Azure unchanged, and dropped for System One
providers.

## Input

`input` is a string, or an array of items in the shape the Responses API uses.

| Item `type` | Fields | Notes |
| - | - | - |
| `message` (the `type` may be left out) | `role` (`user`, `developer`, `system` or `assistant`), `content` (a string or content parts) | `system` is sent as `developer`. An assistant message can carry `phase` (`commentary` or `final_answer`). |
| `function_call` | `call_id`, `name`, `arguments` | A tool call made earlier in the conversation. |
| `function_call_output` | `call_id`, `output` | Every output's `call_id` must match an earlier call. |
| `custom_tool_call` | `call_id`, `name`, `input` | |
| `custom_tool_call_output` | `call_id`, `output` | As `function_call_output`. |

| Content part | Allowed in | Fields | Limits |
| - | - | - | - |
| `input_text` | user, developer and system messages; tool outputs; question instructions | `text` | Up to 10,485,760 characters. |
| `input_image` | user messages, tool outputs, question instructions | `image_url` (a data URL or an `http(s)` URL), `detail` | OpenAI and Azure only; other providers refuse it with `400` `unsupported_input`. No `file_id`. |
| `input_json` | user messages, question instructions | `data`: an object or an array of objects | At most 32 levels deep, 10,000 values and 256 KiB of compact JSON. |
| `output_text`, `refusal` | assistant messages | `text`, `refusal` | |

To decide about a structured record, send it as one `input_json` part:

```json theme={null}
{
  "model": "my-decision-deployment",
  "input": [
    { "role": "user", "content": [{ "type": "input_json", "data": { "order": 1234, "status": "charged twice" } }] }
  ],
  "questions": [{ "type": "predicate", "instructions": "Is this a duplicate charge?" }]
}
```

## Questions

| `type` | Fields | Limits |
| - | - | - |
| `predicate` | `name`, `instructions` | |
| `choice` | `name`, `instructions`, `choices`: `[{ "value", "description" }]` | 2 to 255 choices. `value` is a string or a boolean, unique within the question (`"true"` and `true` are different values). `description` is optional. |
| `score` | `name`, `instructions`, `levels`: `[{ "label", "description" }]` | 2 to 10 levels, lowest first. `description` is optional. |

* `instructions` is required: a non-empty string, or an array of `input_text`, `input_image` and
  `input_json` parts.
* `name` is optional. When set it must be non-empty and unique across the request, and the answer
  carries it back. An unnamed question is answered with `"name": null`.
* Any other field inside a question, a choice or a level is refused with `400` `unknown_parameter`,
  and `param` names it (for example `questions[1].choices[0].weight`).

## Response

| Field | Type | Description |
| - | - | - |
| model | string | The model that answered, as the provider reports it (for example `jev-1.14.0` for a `jev-latest` deployment). |
| answers | array | Exactly one answer per question, in question order. |
| usage | object | `input_tokens`, `input_tokens_details` (`cached_tokens`, `cache_write_tokens`), `output_tokens`, `output_tokens_details.reasoning_tokens` and `total_tokens`. |

| Answer `type` | Fields |
| - | - |
| `predicate` | `name`, `probability`: the probability that the statement holds. |
| `choice` | `name`, `choice` (the chosen value, a string or boolean as it was sent), `confidence`, `probabilities`: one `{ value, probability }` per choice, in request order. |
| `score` | `name`, `score` (the expected level index, the sum of index × probability), `confidence`, `probabilities`: one `{ value, label, probability }` per level, where `value` is the level index. |
| `refusal` | `name`. The model declined this question; the other questions in the request are still answered. Only OpenAI and Azure refuse. |

`confidence` is the provider's own figure, not the largest probability. Probabilities are each
model's own estimates: they are not comparable across models or providers, so set thresholds per
model.

Response headers carry the usual `x-bud-*` set, plus `x-bud-decisions-provider-format` (`openai` or
`system_one`), which says which wire format answered.

## Limits

The gateway checks every request against OpenAI's limits, whichever provider will answer it:

| Limit | Value | Error `param` |
| - | - | - |
| Questions | 1 to 400 | `questions` |
| Choices per question | 2 to 255, unique | `questions[i].choices` |
| Levels per question | 2 to 10 | `questions[i].levels` |
| `input_json` | 32 levels, 10,000 values, 256 KiB | `input[j].content[k].data` |
| `prompt_cache_key` | 64 characters | `prompt_cache_key` |
| `safety_identifier` | 128 characters | `safety_identifier` |
| Tool outputs | every output's `call_id` matches an earlier call | `input[j].call_id` |

A provider with stricter limits is then checked before the call. A request over a provider's limit is
refused with `400` `limit_exceeded`, and the message gives the provider's limit. It is not split into
smaller requests.

## Providers

### OpenAI and Azure OpenAI

The request is forwarded as sent, with `model` replaced by the provider's model name (on Azure, the
deployment name). Images, `prompt_cache_key`, `prompt_cache_options`, `safety_identifier` and
refusals all work as in OpenAI's API.

### System One providers

TypeSafe, servers added with the System One compatible provider, and other System One vendors in the
catalog speak the System One format. Bud translates the request and the answers, with these
differences:

* **No images.** `input_image` parts in the input or in instructions are refused with `400`
  `unsupported_input` before the call.
* **Conversations become text.** An `input` of messages or tool items is sent as a plain-text
  transcript, one block per item (`user: …`, `assistant: …`, `tool_call …`, `tool_result …`). Roles
  survive as text, not as structure. A string input, or a single user message holding a single
  `input_json` part, is sent as is.
* **Descriptions become text.** Choice and level descriptions reach the provider as part of the
  option text; level probabilities are mapped back by index.
* **Cache and safety fields have no effect.** `prompt_cache_key`, `prompt_cache_options`,
  `safety_identifier` and other top-level fields are not sent.
* **No refusals.** System One providers always answer every question.
* **Bearer authentication only.** The provider is called with `Authorization: Bearer`; a server that
  needs another header is not supported.
* **Usage** is the provider's `input_tokens` and `output_tokens`; `cached_tokens`,
  `cache_write_tokens` and `reasoning_tokens` are always `0`.

### TypeSafe

* Models: `jev-latest` (currently `jev-1.14.0`), and the pinned `jev-1.14.0` and `jev-1.13.0`. The
  response `model` is your deployment's name, so deploy a pinned version when you need to know
  which one answered.
* Up to 255 choices and 10 levels per question. TypeSafe sets no question limit of its own, so Bud's
  400 applies.
* Context: 64K tokens per request and 32K per question. TypeSafe's long-context mode is not used.
* TypeSafe reports output tokens (about 20 per question) but does not charge for them: the catalog
  prices input tokens only (USD 0.042 per million at the time of writing), with output at 0.

### Embedding Studio

Embedding Studio is not a System One server; Bud calls its decide API with its own adapter and returns
the same response shape. The System One differences above apply, except authentication: Bud sends
Embedding Studio's own `X-API-Key` header.

* Model: `laya`. Up to 400 questions, 128 choices and 10 levels per question; no images.
* **Priced per request, not per token.** Embedding Studio charges per record whatever the number of
  questions (USD 0.002 in the catalog), and reports no token counts, so every `usage` field is `0`. An
  Embedding Studio deployment is therefore priced per request only: a per-token price would cost every
  call nothing, and is refused. See [Billing](#billing).
* `choice` is the option with the highest probability, and `score` is the expected level index
  computed from the returned distribution. `confidence` is Embedding Studio's own.
* **Abstention is not returned.** Embedding Studio can mark an answer as abstained (with the set of
  options it could not separate, and a review route). The Decisions API has no field for that, so it
  is not passed back: you get the full distribution and can apply your own threshold. Embedding
  Studio projects (fitted heads) and templates are not reachable through Bud.
* Errors keep Embedding Studio's status, with its message cut to 512 characters. A spent account
  balance is returned as `402` `provider_payment_required`.

## Billing

* Cost is input tokens at the deployment's input price (cached tokens at the cached-input price when
  one is set), plus output tokens at the output price. Decision models usually report no output
  tokens, or do not charge for them.
* OpenAI counts cache writes inside `input_tokens`; they are billed at the input price.
* A deployment without a price records no cost.
* A per-request price applies only to a deployment that serves nothing but decisions. A deployment
  that also serves chat is priced per token, so a per-request provider behind it stays unpriced. A
  provider that reports no tokens (Embedding Studio) is priced per request only.
* Every successful call counts toward the project's usage limits. A call over a limit is refused with
  `402` before the provider is called.
* Responses are not cached: identical repeated calls are sent to the provider and billed each time.

## Guardrails

If the deployment has a guardrail profile, the input is checked before the provider is called: string
input, `input_text` parts, `input_json` data, message text, and tool-call arguments and outputs.
Question instructions are not checked. A blocked request returns the same error as a blocked chat
request, and the provider is not called.

## Not supported

* Streaming (`stream: true`).
* Request retries and `fallback_models`: a failed decisions call is not retried and does not fall
  back to another model.
* `box` questions, and images on System One providers.

## Errors

Errors use OpenAI's error format; `param` names the field at fault when there is one.

```json 400 theme={null}
{
  "error": {
    "message": "Question names must be unique within the request.",
    "type": "invalid_request_error",
    "param": "questions[1].name",
    "code": "invalid_value"
  }
}
```

| Status | `code` | When |
| - | - | - |
| 400 | `invalid_json` | The body is not JSON. |
| 400 | `stream_not_supported` | `stream` is present and truthy. |
| 400 | `invalid_value`, `unknown_parameter` | A field breaks the rules in [Input](#input), [Questions](#questions) or [Limits](#limits); `param` names it. |
| 400 | `unsupported_input` | An image part for a provider that cannot take one, a boolean and a string choice value that collide (`true` and `"true"`) on a System One provider, or another feature the provider cannot carry. |
| 400 | `limit_exceeded` | Over the provider's own limit; the message gives the limit. |
| 400 | capability error | The model exists but does not serve the Decisions API. |
| 400 | guardrail error | The input guardrail blocked the request, as for chat. |
| 401, 403 | | Authentication or permission failure. |
| 402 | | The project's usage limit is reached. |
| 404 | `model_not_found` | No such model for this API key. |
| 429 | | Rate limited. |
| 4xx | provider's | The provider refused the request (for example a TypeSafe validation error); its status and message are passed through. |
| 502 | `invalid_provider_response` | The provider returned the wrong number, order or type of answers. No partial response is returned. |
| 5xx | | Every provider for the deployment failed. |


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