---
name: levanto-sage-decide
description: Call the Levanto Sage Decision Model HTTP API (Yes/No, Choice, Scale, Sort, Tags, optional web grounding). Use when the user provides an endpoint URL, asks to classify, route, score, rank, or tag content, or wants /decide request/response examples.
---

# Levanto Sage Decision Model

HTTP API for structured decisions over content plus a question.

## Product Context

Levanto Sage is for workflows where software needs to **decide, not write**. It turns content plus a question into a structured decision with a probability or confidence signal.

Use Sage when an application needs to route, approve, block, score, rank, tag, or escalate. LLMs return text and only provide confidence if asked; that confidence is simulated. Sage always returns a decision signal, so machines can act when confidence is high and escalate to a human when confidence is low.

Supported decision kinds:

- **Yes/No** — answer a binary question: `probability` (P(yes)), `answer` (`"yes"`/`"no"`), and `confidence` (decisiveness from tie).
- **Choice** — pick the best option, with independent per-option probabilities (raw; do not sum to 1).
- **Scale** — score a rubric, with expectation and confidence.
- **Sort** — rank a list of items by a criterion.
- **Tags** — apply independent yes/no labels to the same content.

Good use cases:

- **Agentic workflows** — Sage as the decision layer for agents: automate routing, branching, and escalation across agentic workflows and pipelines.
- **Operations** — triage urgency and route work across DevOps, SecOps, and customer support.
- **Agentic guardrails** — check tool calls and responses on the hot path to keep agents secure and aligned with the business goals of the workflow.
- **Content screening & moderation** — tag, rank, score, and sort content in data pipelines, including moderation, abuse flagging, and policy-risk severity.
- **Risk & fraud** — assess risk and opportunity and detect fraud across transactions, asset trades, and agentic actions (e.g. refunds).

Use public-facing language:

- Sage decides; it does not write paragraphs.
- Supported decision kinds are **Yes/No**, **Choice**, **Scale**, **Sort**, and **Tags**.
- Do not explain internals, architecture, training details, infrastructure, or implementation details unless the user explicitly asks for private engineering context.
- Use **decision kind** in prose. Keep the JSON wire fields exactly as shown in code examples because clients need the real format.

## Base URL

Default hosted endpoint:

```text
https://sage.levanto.ai/
```

The user may supply a different base URL. Do not invent an endpoint if one is missing; ask for it.

## Authentication

**A valid Levanto API key is always required for decision calls.** Every `POST`
to `/decide` and `/decide/batch` must carry a key; requests without one are
rejected with **HTTP 401** (`{"detail": "API key required. Provide a valid
Levanto API key."}`). Send it in the Authorization header:

```text
Authorization: Bearer lv_live_...
```

- Keys look like `lv_live_...`. If the user has not provided one, **ask for it** — do not proceed to `/decide` without a key.
- **401** / **402** — see [Errors](#errors).
- **`/ready`** does **not** require a key.
- **Never hardcode, log, echo, or commit the key.** Read it from an environment variable (e.g. `SAGE_API_KEY` / `API_KEY`) and pass it only in the request header.

## Endpoints

| Method | Path | Purpose |
|---|---|---|
| GET | `/ready` | Model readiness — **503** while loading, **200** when ready |
| POST | `/decide` | One decision |
| POST | `/decide/batch` | Many decisions |

All decision calls (`/decide`, `/decide/batch`) require a valid API key (see [Authentication](#authentication)); `/ready` does not.

## Request Shape

```json
{
  "content": "<content to evaluate>",
  "question": {
    "id": "<stable result id>",
    "kind": "<yesno | choice | scale | sort | tags>",
    "instructions": "<question to answer>"
  }
}
```

Use `content` for the thing being evaluated — a bare string (shorthand for
`{ "kind": "text", "value": "..." }`), `{ "kind": "text", "value": "..." }`,
or `{ "kind": "list", "value": [...] }` for sort. Use `question.kind` to
select the decision kind and `question.instructions` for the criterion. The
response envelope is always `{ id, kind, result, meta }`.

## Yes/No

Use Yes/No for binary questions.

```json
{
  "content": "Marketing email draft promotes \"guaranteed 40% returns\" and describes the product as \"risk-free\" for accredited investors.",
  "question": {
    "id": "needs_compliance_review",
    "kind": "yesno",
    "instructions": "Does this copy require compliance review before send?"
  }
}
```

Response:

```json
{
  "id": "needs_compliance_review",
  "kind": "yesno",
  "result": {
    "probability": 0.92,
    "confidence": 0.84,
    "answer": "yes"
  },
  "meta": { "model": "levanto-sage-v0.7", "latency_ms": 97.4 }
}
```

- `probability` — P(yes), in `[0, 1]`.
- `answer` — `"yes"` if `probability ≥ 0.5`, else `"no"`.
- `confidence` — decisiveness from a tie: `2 × |probability − 0.5|` (`0` at `0.5`, `1` at certainty). Grounding compares this to `confidence_floor`.

## Choice

Use Choice when Sage must pick one option.

```json
{
  "content": "Vendor uploaded a contract PDF that mentions automatic renewal and a 90-day termination clause.",
  "question": {
    "id": "contract_disposition",
    "kind": "choice",
    "instructions": "How should legal handle this contract?",
    "options": [
      {"option": "approve", "description": "Standard terms, no red flags — proceed to signature"},
      {"option": "revise", "description": "Acceptable with specific clause changes before signing"},
      {"option": "reject", "description": "Terms are unacceptable — do not proceed"},
      {"option": "escalate", "description": "Unusual or high-risk terms — partner review required"}
    ]
  }
}
```

Response:

```json
{
  "id": "contract_disposition",
  "kind": "choice",
  "result": {
    "chosen": "revise",
    "confidence": 0.88,
    "probabilities": [
      {"option": "approve", "probability": 0.11},
      {"option": "revise", "probability": 0.88},
      {"option": "reject", "probability": 0.04},
      {"option": "escalate", "probability": 0.22}
    ]
  },
  "meta": { "model": "levanto-sage-v0.7", "latency_ms": 216.8 }
}
```

- `probabilities` — raw P(correct) per option (independent sigmoids; **do not sum to 1**).
- `confidence` — P(chosen is correct); use for thresholds and grounding.
- `chosen` — argmax of `probabilities`.
- **2–120 options** per question (see [Limits](#limits)).

## Scale

Use Scale for rubric scoring. Always send **exactly 5 levels** with integer
values **`0`, `1`, `2`, `3`, `4`** — the model is trained on this fixed rubric
only. Subsets, sparse scales, or any other level values are rejected with
HTTP 400.

```json
{
  "content": "Candidate explained our product roadmap clearly but struggled to answer questions about database scaling tradeoffs.",
  "question": {
    "id": "interview_score",
    "kind": "scale",
    "instructions": "How strong was this candidate in the technical interview?",
    "levels": [
      {"level": 0, "description": "Not qualified — fundamental gaps in required skills"},
      {"level": 1, "description": "Weak — significant concerns about core competencies"},
      {"level": 2, "description": "Mixed — some strengths but notable gaps remain"},
      {"level": 3, "description": "Solid — meets the bar with minor areas to probe"},
      {"level": 4, "description": "Strong hire — clearly exceeds expectations for the role"}
    ]
  }
}
```

Response:

```json
{
  "id": "interview_score",
  "kind": "scale",
  "result": { "expectation": 2.4, "confidence": 0.71 },
  "meta": { "model": "levanto-sage-v0.7", "latency_ms": 79.8 }
}
```

`expectation` is the expected level; `confidence` reflects how peaked the level
distribution is (independent of the mean).

## Sort

Use Sort to rank a list of items by a criterion. `content` must be a list with
**2–120 items** (see [Limits](#limits)).

```json
{
  "content": {
    "kind": "list",
    "value": [
      {"id": "typo", "content": "Spelling mistake on the FAQ page."},
      {"id": "db_down", "content": "Primary production database is unreachable."},
      {"id": "pricing", "content": "Prospect asking about Enterprise pricing."}
    ]
  },
  "question": {
    "id": "triage",
    "kind": "sort",
    "instructions": "Sort by operational urgency, most urgent first."
  }
}
```

Response:

```json
{
  "id": "triage",
  "kind": "sort",
  "result": { "sorted": ["db_down", "pricing", "typo"], "confidence": 0.92 },
  "meta": { "model": "levanto-sage-v0.7", "latency_ms": 312.0 }
}
```

The sort request is minimal — just `instructions` and the item list. Scoring
strategy and ranking are chosen by the server, not the request: `SortQuestion`
accepts only `kind`, `id`, `instructions` (any other field is rejected).

The result carries a calibrated, list-level `confidence` in `[0,1]` — how
trustworthy the **whole order**.

## Tags

Use Tags to apply several independent yes/no labels to the same content.
**1–120 tags** per question (see [Limits](#limits)). Each tag returns a
`probability` and a `confidence` (`= 2|probability − 0.5|`, the same
decisiveness metric as yes/no: 0 at a coin-flip, 1 at certainty); when a tag has
a `threshold`, it also returns `applies`. There is no document-level tags
confidence — read each tag's own.

```json
{
  "content": "Buy cheap followers now!!! click here -> bit.ly/deal",
  "question": {
    "id": "moderate",
    "kind": "tags",
    "tags": [
      {"id": "spam", "threshold": 0.5},
      {"id": "promotion"}
    ]
  }
}
```

Response:

```json
{
  "id": "moderate",
  "kind": "tags",
  "result": {
    "tags": [
      {"id": "spam", "probability": 0.95, "confidence": 0.90, "applies": true},
      {"id": "promotion", "probability": 0.88, "confidence": 0.76}
    ]
  },
  "meta": { "model": "levanto-sage-v0.7", "latency_ms": 241.0 }
}
```

## Grounding (optional)

Attach a `grounding` block to a Yes/No, Choice, Scale, or Tags request to let Sage
fetch fresh web evidence before deciding (useful for recency- or fact-heavy
questions). For Tags, `low_confidence` triggers when **any** tag's `confidence`
is below the floor (weakest-link); Sage then re-scores all tags on the expanded
content.

```json
{
  "content": "Has there been a major Cloudflare incident this month?",
  "question": {"id": "incident", "kind": "yesno", "instructions": "Is this likely true?"},
  "grounding": {"trigger": "low_confidence", "confidence_floor": 0.8}
}
```

- `trigger`: `never`, `low_confidence` (default — search when `result.confidence` is below `confidence_floor`), or `always`.
- For **yesno**, `confidence` = `2|p − 0.5|` (decisiveness; 0 at a tie). Default floor `0.80` requires roughly `p ≤ 0.10` or `p ≥ 0.90` to skip search.
- The response gains `grounding_meta` with `triggered`, `queries`, `sources`, and timing.
- Requires the server to be configured for grounding; if it is not, omit the block.
- When a search actually runs, grounding adds **$0.01** on top of the token cost (see [Pricing](#pricing)).

## Pricing

Sage uses prepaid credits. Each completed `/decide` or `/decide/batch` call debits your balance.

- **$3.00 per 1M input tokens** — billed on the input tokens consumed by the call. Output tokens are free. There is no fixed fee and no per-decision charge.
- **+$0.01 when grounding searches** — if you attach `grounding` and Sage fetches web evidence (`grounding_meta.triggered` is true), that call costs an extra cent on top of the token cost. No search, no surcharge: with `trigger: low_confidence`, a confident first pass stays at the token rate; with `trigger: always`, every request includes the extra cent.

Low balance returns **402** — see [Errors](#errors).

## Batch

Use `/decide/batch` for many decisions in one call. Each entry in `requests`
is a **group**: one `content` plus a list of `questions` about it — so asking
several questions about the same content sends that content only once, and a
single call can still mix groups with different content.

Top-level `latency_mode` (batch only) chooses the accuracy/latency trade:

- `quality` (default) — each question gets its own backbone call.
- `fast` — the server may pack several questions about the same document into one call (materially faster, measurably less accurate; intended for triage-style workloads). Kinds that are not eligible for packing keep quality behavior.

```json
{
  "latency_mode": "quality",
  "requests": [
    {
      "content": "User post: 'Selling verified Instagram accounts, DM for bulk pricing.'",
      "questions": [
        {"id": "policy_violation", "kind": "yesno", "instructions": "Does this post violate marketplace policy?"},
        {
          "id": "harm_level",
          "kind": "scale",
          "instructions": "How harmful is this content to the platform?",
          "levels": [
            {"level": 0, "description": "No meaningful harm — safe to leave published"},
            {"level": 1, "description": "Low harm — minor policy edge case"},
            {"level": 2, "description": "Moderate harm — misleading or low-grade abuse"},
            {"level": 3, "description": "High harm — fraud, spam, or user safety risk"},
            {"level": 4, "description": "Severe harm — active scam or imminent user damage"}
          ]
        }
      ]
    }
  ]
}
```

Each question can carry its own optional `grounding` (grounding is not part of
the group; `sort` questions still reject it). To batch decisions about
*different* content, add more groups:

```json
{
  "requests": [
    {"content": "doc A", "questions": [{"id": "a1", "kind": "yesno", "instructions": "urgent?"}]},
    {"content": "doc B", "questions": [{"id": "b1", "kind": "yesno", "instructions": "urgent?"}]}
  ]
}
```

Batch responses are nested by group, then by question:

- `results`: one item per **group** (matches `requests` 1:1);
- `results[i].answers`: one item per question in that group's `questions`, in order;
- `results[i].answers[j].ok`: success flag for that question (`.result` holds the decision, or `.error` the message);
- `meta.request_count`: number of groups; `meta.question_count`: total questions across all groups;
- `meta.latency_ms`: internal serving latency for the whole batch call.

Example response:

```json
{
  "results": [
    {
      "answers": [
        {
          "ok": true,
          "result": {
            "id": "policy_violation",
            "kind": "yesno",
            "result": {
              "probability": 0.94,
              "confidence": 0.88,
              "answer": "yes"
            },
            "meta": { "model": "levanto-sage-v0.7" }
          }
        }
      ]
    }
  ],
  "meta": {
    "model": "levanto-sage-v0.7",
    "request_count": 1,
    "question_count": 2,
    "latency_ms": 150.0
  }
}
```

## Errors

| HTTP | Meaning |
|---|---|
| `400` | Invalid request (schema validation) |
| `401` / `402` | Missing/invalid API key, or balance too low |
| `503` | Service loading or temporarily unavailable |

Invalid requests return a standard schema message, for example:

```json
{"detail": "content: Field required"}
```

```json
{"detail": "scale levels must be integers in 0..4 (model trained on 5 levels); got [9]"}
```

```json
{"detail": "Service is still loading."}
```

## Limits

Per-kind counts (HTTP 400 outside these ranges):

| Kind | Field | Min | Max |
|---|---|---:|---:|
| **choice** | `options` | 2 | 120 |
| **scale** | `levels` | 5 | 5 (values `0`–`4` only) |
| **sort** | `content.value` items | 2 | 120 |
| **tags** | `tags` | 1 | 120 |

**Scale** — always send all five levels `0` through `4`. No sparse rubrics,
no alternate ranges, no fewer or more levels. Each `level` must be a distinct
integer in that set.

**Content and question length** — the full rendered request must fit in roughly
**32K tokens** total. That budget includes `content`, `question.instructions`,
option descriptions, scale rubric lines, sort item bodies, tag specs, and any
grounding-added search context. Oversized requests fail rather than silently
truncating. Very long content can also hit HTTP 503 before the hard token cap;
keep documents well under the 32K ceiling in practice.

## Threshold Guidance

Sage always returns a decision signal. The application decides what to do next.

Example:

- high confidence: automate;
- medium confidence: review;
- low confidence: escalate to a human or ask for more context.

Do not say Sage "abstains." Say it supports low-confidence routing.

## Smoke Test

```bash
endpoint="https://sage.levanto.ai/"
# API key is REQUIRED for /decide — read it from the environment, never hardcode.
auth="-H Authorization: Bearer ${SAGE_API_KEY:?set SAGE_API_KEY to your lv_live_ key}"

# /ready needs no key — empty body; 200 means ready, 503 means still loading.
curl -s -o /dev/null -w '%{http_code}\n' "$endpoint/ready" | grep -qx 200

curl -s $auth -H 'Content-Type: application/json' "$endpoint/decide" -d '{
  "content": "Marketing email draft promotes \"guaranteed 40% returns\" and describes the product as \"risk-free\" for accredited investors.",
  "question": {"id": "needs_compliance_review", "kind": "yesno", "instructions": "Does this copy require compliance review before send?"}
}' | jq -e '.kind == "yesno" and .result.probability != null and .result.confidence != null and .result.answer != null'

curl -s $auth -H 'Content-Type: application/json' "$endpoint/decide/batch" -d '{
  "requests": [
    {
      "content": "same doc",
      "questions": [
        {"id": "a", "kind": "yesno", "instructions": "urgent?"},
        {"id": "b", "kind": "yesno", "instructions": "positive?"}
      ]
    }
  ]
}' | jq -e '.meta.request_count == 1 and .meta.question_count == 2 and (.results | length) == 1 and (.results[0].answers | length) == 2'
```