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

# Classify

> Answer yes/no, multiple-choice and scored questions about any text or JSON state with a classification model through the AI Gateway.

<Note>
  Classify is in Beta, and its request and response contract matches the [TypeSafe Classify API](https://docs.typesafe.ai/api).

  * Answers natively on OpenAI GPT-6 Luna and `typesafe/jev-latest`, and through structured outputs on a set of small chat models: see [Supported models](#supported-models).
  * Does not apply PII plugins, guardrails, or evaluators yet: see [Enforcement during the Beta](#enforcement-during-the-beta).
  * Requires an API key with the `classify` permission.
</Note>

## Overview

**Classify** answers typed questions about a piece of content in one call. Send the content as `state` and one or more named `questions` to [`POST /decisions`](/reference/decisions/create-decisions) or [`POST /classify`](/reference/classify/create-classify) on the **AI Gateway** base URL, `https://my.orq.ai/v3/router`.

Both endpoints accept the same request and return the same response. Existing `/classify` clients can continue using that path. The SDK methods are `orq.router.decisions.create()` and `orq.router.classify.create()`.

The response returns one structured answer per question, so nothing has to be parsed out of free-form text. A `choice` or `score` answer also carries the probabilities behind it and a confidence for the selected option or level.

A question pairs plain-language `instructions` with a `type` that fixes the answer shape, and `criteria` that define the rubric:

| `type` | What the answer is | Reach for it when |
| - | - | - |
| `noul` | The probability that the statement in `instructions` is true, in `[0, 1]` | Yes/no decisions, or a probability to threshold instead of a hard label |
| `choice` | One option from a named rubric, with a probability for each option | The content belongs to one of a known set: a queue, a topic, a category |
| `score` | A position on an ordered scale, with a probability for each level | The answer is a degree rather than a category: sentiment, quality, effort |

`state` is not limited to text: an object or array is serialized as JSON before classification, so the same call can judge a customer message, a tool payload, or a whole record.

Several questions can be asked at once, each with its own rubric, so a request costs one round trip instead of one call per question. See [Question types](#question-types) for the `criteria` each type expects.

The request is traced, priced, and rate-limited like other gateway traffic. See [Enforcement during the Beta](#enforcement-during-the-beta) for what applies today.

## Use cases

* **Triage and routing.** Ask `noul` whether the message reports a problem and `choice` for the topic or product, then route on the labels and escalate the rows whose confidence is low.
* **Building labelled data.** Turn a rubric into a `choice` question to label a corpus for [Evaluators](/ai-studio/optimize/evaluators) or [Datasets](/ai-studio/optimize/datasets), and use `probabilities` to pick the rows worth human review.
* **Scoring open feedback.** Use `score` on survey answers, support transcripts, or review text to get an ordinal reading without writing a scoring prompt.
* **Gating expensive work.** Use a `noul` question on the native model, whose output tokens are free, as a pre-check, and send only the requests that pass on to a larger model.
* **Enriching structured records.** Pass a record as `state` and ask one question per field to derive, instead of building a prompt per field.
* **Comparing models on one rubric.** Ask the same questions against `typesafe/jev-latest` and a chat model to see where they disagree. Thresholds tuned on one do not transfer, so compare answers rather than raw probabilities.

## Supported models

| Model | How it answers | Output tokens |
| - | - | - |
| `openai/gpt-6-luna`, `openai/eu.gpt-6-luna` | Native OpenAI Decisions. Returns provider probabilities and confidence. | Free |
| `typesafe/jev-latest` | Native classify model. Probabilities are calibrated. | Free |
| `anthropic/claude-haiku-4-5`, also as `google/claude-haiku-4-5` and the Bedrock `aws/...claude-haiku-4-5...` profiles | One structured-output call; the model reports its own probabilities. | Billed at the model rate |
| `google-ai/gemini-3.8-flash`, `google/gemini-3.8-flash` | One structured-output call; the model reports its own probabilities. | Billed at the model rate |
| `zai/glm-5.3-flash` | One forced tool call; the model reports its own probabilities. | Billed at the model rate |
| `openai/gpt-5.6-luna`, also as `azure/gpt-5.6-luna` | One structured-output call; the model reports its own probabilities. | Billed at the model rate |
| `groq/openai/gpt-oss-20b`, `groq/openai/gpt-oss-120b`, `groq/openai/gpt-oss-safeguard-20b`, `groq/qwen/qwen3.8-27b` | One structured-output call; the model reports its own probabilities. | Billed at the model rate |

The table lists model families. Every offering of these models, including regional and reseller variants, is in [Supported models](/ai-gateway/supported-models#classify-models).

Chat models return the same fields with their own probabilities, which express the model's estimate and are not calibrated, so thresholds tuned on one model do not transfer to another.

For emulated classification, reasoning is set to the minimum the model allows.

OpenAI Decisions accepts text or arrays of user messages with `input_text` and inline base64 `input_image` parts in `state`. Hosted image URLs, file IDs, and non-user roles are rejected. Content parts must be nested inside user messages; bare content parts and tool items are rejected rather than serialized as text. Other objects and arrays are serialized as text. OpenAI can return `{"type": "refusal"}` under an individual question key; handle refusals before reading scored fields.

Any other model returns `400`.

## Quick start

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST https://my.orq.ai/v3/router/classify \
    -H "Authorization: Bearer $ORQ_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "typesafe/jev-latest",
      "state": "The package arrived two days late and the box was damaged, but support refunded me right away.",
      "questions": {
        "is_complaint": {
          "type": "noul",
          "instructions": "Does the customer report a problem with their order?",
          "criteria": {
            "true": "A problem with the order is described",
            "false": "No problem is described"
          }
        },
        "topic": {
          "type": "choice",
          "instructions": "What is the main topic of the message?",
          "criteria": {
            "shipping": "Delivery timing or packaging",
            "billing": "Charges, refunds or invoices",
            "other": null
          }
        },
        "satisfaction": {
          "type": "score",
          "instructions": "How satisfied is the customer overall?",
          "criteria": ["Unhappy", "Neutral", "Satisfied"]
        }
      }
    }'
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import { Orq } from "@orq-ai/node";

  const orq = new Orq({
    apiKey: process.env.ORQ_API_KEY ?? "",
  });

  const result = await orq.router.classify.create({
    model: "typesafe/jev-latest",
    state:
      "The package arrived two days late and the box was damaged, but support refunded me right away.",
    questions: {
      is_complaint: {
        type: "noul",
        instructions: "Does the customer report a problem with their order?",
        criteria: {
          true: "A problem with the order is described",
          false: "No problem is described",
        },
      },
      topic: {
        type: "choice",
        instructions: "What is the main topic of the message?",
        criteria: {
          shipping: "Delivery timing or packaging",
          billing: "Charges, refunds or invoices",
          other: null,
        },
      },
      satisfaction: {
        type: "score",
        instructions: "How satisfied is the customer overall?",
        criteria: ["Unhappy", "Neutral", "Satisfied"],
      },
    },
  });

  console.log(result.answers);
  console.log(result.usage);
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  from orq_ai_sdk import Orq
  import os

  orq = Orq(api_key=os.environ.get("ORQ_API_KEY", ""))

  result = orq.router.classify.create(
      model="typesafe/jev-latest",
      state="The package arrived two days late and the box was damaged, but support refunded me right away.",
      questions={
          "is_complaint": {
              "type": "noul",
              "instructions": "Does the customer report a problem with their order?",
              "criteria": {
                  "true": "A problem with the order is described",
                  "false": "No problem is described",
              },
          },
          "topic": {
              "type": "choice",
              "instructions": "What is the main topic of the message?",
              "criteria": {
                  "shipping": "Delivery timing or packaging",
                  "billing": "Charges, refunds or invoices",
                  "other": None,
              },
          },
          "satisfaction": {
              "type": "score",
              "instructions": "How satisfied is the customer overall?",
              "criteria": ["Unhappy", "Neutral", "Satisfied"],
          },
      },
  )

  print(result.answers)
  print(result.usage)
  ```
</CodeGroup>

## Request

| Field | Type | Required | Description |
| - | - | - | - |
| `model` | string | Yes | One of the [supported models](#supported-models). |
| `state` | string, object or array | Yes | The content to classify. Objects and ordinary arrays are serialized as JSON. Native OpenAI also accepts user-message arrays with text and inline images. |
| `questions` | object | Yes | Maps each question key to its definition. The response uses the same keys in `answers`. |
| `name` | string | No | Name shown on the trace. A default name is used when omitted. |
| `metadata` | object | No | Key-value pairs attached to the trace, for filtering later. |
| `fallbacks` | array | No | Up to 10 ordered fallback models, each with a `model` ID. Every model must support classification and pass access checks. Inline-image requests require native OpenAI fallbacks. |
| `retry` | object | No | Retry configuration for this request: `count` (1 to 5) and `on_codes`, the status codes that trigger a retry. |
| `identity` | object | No | Attributes the request to an end user, like the other gateway endpoints: the body identity takes precedence over `X-ORQ-IDENTITY-ID` and the API-key owner. See [Request Metadata](/ai-gateway/request-metadata). |

Each question has a `type` and `instructions`, which can be a string, object, or array. The shape of `criteria` depends on the question type.

### Question types

#### `noul`

Returns the probability that the statement in `instructions` is true for `state`. The probability is calibrated on `typesafe/jev-latest` and model-reported on chat models.

The optional `criteria` object can define what counts as `true` and `false`.

```json JSON theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "noul",
  "instructions": "Does the customer report a problem with their order?",
  "criteria": {
    "true": "A problem with the order is described",
    "false": "No problem is described"
  }
}
```

#### `choice`

Selects one option from a rubric. The required `criteria` object maps each option name to a description. Set a description to `null` when the option name is enough for the model to interpret it.

```json JSON theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "choice",
  "instructions": "What is the main topic of the message?",
  "criteria": {
    "shipping": "Delivery timing or packaging",
    "billing": "Charges, refunds or invoices",
    "other": null
  }
}
```

#### `score`

Returns a position on an ordered scale. Set `criteria` to an array with at least two level descriptions, ordered from lowest to highest. The answer index refers to this array.

```json JSON theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "score",
  "instructions": "How satisfied is the customer overall?",
  "criteria": ["Unhappy", "Neutral", "Satisfied"]
}
```

## Response

Native providers return their own `confidence` and `score`. Emulated classification uses one structured-output call. OpenAI Decisions returns a probability-weighted score and preserves its own confidence. The fields below describe these provider differences.

```json JSON theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "model": "typesafe/jev-latest",
  "answers": {
    "is_complaint": { "type": "noul", "noul": 0.97 },
    "topic": {
      "type": "choice",
      "choice": "shipping",
      "probabilities": { "shipping": 0.79, "billing": 0.2, "other": 0.01 },
      "confidence": 0.68
    },
    "satisfaction": {
      "type": "score",
      "score": 1.41,
      "legend": { "0": "Unhappy", "1": "Neutral", "2": "Satisfied" },
      "probabilities": { "0": 0.03, "1": 0.52, "2": 0.45 },
      "confidence": 0.28
    }
  },
  "usage": {
    "input_tokens": 412,
    "output_tokens": 0,
    "input_cost": 0.000017304,
    "output_cost": 0,
    "total_cost": 0.000017304
  },
  "telemetry": {
    "trace_id": "4f2294c4bd55372a354be496871aa917",
    "span_id": "a354be496871aa91"
  }
}
```

| Field | Description |
| - | - |
| `model` | The requested ID of the model that answered, including a fallback model when one was used. |
| `answers` | One entry for each question key. Each entry repeats the question `type`. |
| `answers.<key>.noul` | `noul` only. Probability in the range `[0, 1]` that the statement holds. |
| `answers.<key>.choice` | `choice` only. The selected option name. |
| `answers.<key>.score` | `score` only. Position on the scale, not an index. On emulated chat models and OpenAI Decisions it is the weighted index: each level index multiplied by that level's probability, summed, so a three-level answer lands on a fraction such as `1.42` for the distribution above. `typesafe/jev-latest` returns its own score instead, which is why that answer shows `1.41` beside a distribution whose weighted index is `1.42`. |
| `answers.<key>.type` | `noul`, `choice`, `score`, or `refusal`. Refusals contain no scored fields. |
| `answers.<key>.legend` | `score` only. Maps each level index to its description in the `criteria` array. |
| `answers.<key>.probabilities` | `choice` and `score`. Probability for each option name or level index. |
| `answers.<key>.confidence` | `choice` and `score`. How sure the model is of the answer it selected. On emulated chat models it is the largest value in `probabilities`, so it carries no information beyond them. `typesafe/jev-latest` returns its own calibrated value instead, which can sit below the largest probability: in the example above, `0.68` against `0.79` for the `choice` answer and `0.28` against `0.52` for the `score` answer. |
| `usage.input_tokens` | Total input tokens, including cached tokens. |
| `usage.input_tokens_details` | Optional provider-reported `cached_tokens` and `cache_write_tokens`. OpenAI Decisions does not charge for these tokens. |
| `usage.output_tokens` | Output tokens. Free for native classify providers, billed at the model rate for emulated chat models. |
| `usage.input_cost` | Cost in USD of the input tokens. Present when billing was computed. |
| `usage.output_cost` | Cost in USD of the output tokens. `0` for native classify providers. Present when billing was computed. |
| `usage.total_cost` | Total cost in USD. This follows the same format as the Responses API `usage` fields. |
| `telemetry.trace_id` / `telemetry.span_id` | Identifiers of the trace and root span recorded for the request. The same values are returned in the `x-orq-trace-id` and `x-orq-trace-span-id` headers. |

The response includes optional `telemetry`. Read `x-orq-trace-id` and `x-orq-trace-span-id` from the response headers when older SDKs or deployments omit it. The handler sets those once the request is traced, so a request rejected before that point carries neither: authentication and authorization (`401`, and `403` for a missing `classify` permission or a model that is not enabled for the workspace or shared with the project), plan and budget limits (`429`), and request-body validation (the `400` for a malformed body, the `422` for an invalid request). Everything rejected after tracing carries them, including the `400` for a model that does not support classify. The `telemetry` object itself is optional and returned by builds newer than some deployments run, so fall back to the headers when it is absent.

Emulated chat models return the same scored fields, but the numbers do not mean the same thing: their probabilities are the model's own estimates and are not calibrated, and their `confidence` is simply the largest of them. Compare `choice` and `score` across models, not `confidence`.

See the [Create Classify](/reference/classify/create-classify) API reference for the full request and response schema, including the fields accepted alongside the ones above.

## Fallbacks, retries and identity

Use the same options on `/decisions` and `/classify`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "model": "openai/gpt-6-luna",
  "state": "The customer loves the product.",
  "questions": {
    "positive": { "type": "noul", "instructions": "Is the sentiment positive?" }
  },
  "fallbacks": [{ "model": "openai/gpt-5.6-luna" }],
  "retry": { "count": 2, "on_codes": [429, 502, 503, 504] },
  "identity": { "id": "customer-demo", "display_name": "Sample customer" }
}
```

`retry.count` is the number of retries after the first attempt, from 1 to 5. Retries happen only for status codes in `on_codes`; omitting `retry` disables gateway retries. The gateway honors `Retry-After` or waits one second when it is absent. After retries are exhausted, it tries the next fallback. A request accepts at most 10 fallbacks. The response reports the successful model and that model's costs. A chat fallback pays the chat model's input and output rates.

For inline-image requests, every fallback must support native OpenAI image classification; incompatible chains are rejected with `422` before a provider call.

An inaccessible fallback stops the remaining fallback chain. Session keys must allow every requested model under their factory policy.

## Enforcement during the Beta

Enforced on every request:

| What | Effect |
| - | - |
| Authentication | A missing or invalid key is rejected with `401`. |
| The `classify` permission | A key without it is rejected with `403`. The standard gateway key preset grants it. |
| Model access | The model must be enabled for the workspace or shared with the project, otherwise the request is rejected with `403`; it must also support classify, otherwise the request is rejected with `400`. |
| Plan rate limits and budgets | The request counts toward both, and a rejection returns `429` exactly as on other endpoints. See [Rate limits and quotas](/ai-gateway/features/rate-limits). |
| Tracing, usage, and cost | The request is traced and billed, and the response reports `usage`; `telemetry` is optional. |
| Trace masking | Masks configured through workspace plugins apply to the stored trace. On chat models, the instructions that drive the structured answer are always scrubbed from the model span. |
| Request-level `retry` | Retries each model for matching `on_codes` before moving to its fallback. |
| Request-level `fallbacks` | Tries models in the supplied order when the previous model fails. Every model must support classify and pass access checks. A refusal is a successful answer and does not trigger fallback. |
| Identity | Body `identity.id`, then `x-orq-identity-id`, then the API-key owner identity determine trace attribution. Body identity details are published for upsert through the standard gateway identity middleware. |

Not applied during the Beta:

* **PII plugins.** `state` and `instructions` reach the provider exactly as sent, even when a redaction plugin is configured for traffic that would otherwise match.
* **Guardrails and evaluators.** They do not run, so a matching guardrail rule cannot block a Classify request.
* **Routing rules.** A rule does not rewrite the model, add fallbacks, or serve from cache.
* **Request-level `load_balancer`, `timeout`, `cache`, `plugins`, and `guardrails`.** They are not part of this contract. Unknown body fields are ignored rather than rejected, so sending them has no effect and returns no error.

## Pricing and limits

| Model | Input | Output | Limits |
| - | - | - | - |
| `typesafe/jev-latest` | \$0.042 per 1M tokens | Free | 64k context window, and `state` plus the longest question must fit within 32k tokens |
| OpenAI Decisions | $0.10 per million input tokens; $0.20 above 272K input tokens. EU rates include a 10% premium | Free, including cache reads and writes | Only `gpt-6-luna` |
| Emulated chat models | The model's own rate | The model's own rate | The model's limits from the Model Garden. The structured answer adds a few hundred output tokens per request |

## Errors

| Status | Cause |
| - | - |
| `400` | Malformed JSON body, an invalid retry configuration, an empty fallback model, more than 10 fallbacks, or a model is not a [supported model](#supported-models). |
| `401` | The API key is missing, invalid, or revoked. |
| `403` | The key is valid but lacks the `classify` permission, or the model is not enabled for the workspace or shared with the project. |
| `422` | The JSON is valid, but the classification structure is not. For example, a `choice` question has no `criteria`, or a `score` question has fewer than two levels, or an image request has an incompatible fallback. |
| `429` | A plan rate limit or a budget limit was reached before the provider was called. See [Rate limits and quotas](/ai-gateway/features/rate-limits). |
| `429` / `529` | The upstream provider reached a rate limit or is overloaded. The response keeps the upstream status and `Retry-After` header. Retry after the specified delay. |
| `500` | An internal model-resolution error occurred. Internal infrastructure details are logged without being returned in the response. |


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