# Laya — typed decisions with calibrated probabilities

**Laya** (Convai Innovations, Apache-2.0) is a *decision* model, not a chat model.
You send it a **state** — a ticket, an email, a conversation, a JSON object — and
a set of **typed questions**. It answers every question in one forward pass, each
with a calibrated probability, and never generates text. Use it to route, triage,
score and flag at a fraction of an LLM call.

It is a model in the catalog: **call it from code with your account key and a
`model` id**, the way you call an LLM — nothing to deploy. Two versions:

| `model` | Languages | Reads | Backbone |
|---|---|---|---|
| **`laya`** | English | 512 tokens of state | ModernBERT-large (421M) |
| **`laya-multilingual`** | 100+ | 1,024 tokens of state | mmBERT-base (322M) |

Longer states are truncated; the response tells you when (`usage.truncated`).
`GET /v1/decisions/models` lists them. There is no per-call charge today: Laya runs
on offrec's own CPU.

## 1. Call it

```bash
curl https://api.offrec.ai/v1/decisions \
  -H "Authorization: Bearer offrec_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "laya",
    "state": "Checkout has been down for an hour and we are losing sales. Fix it or we cancel.",
    "questions": {
      "queue": {"type": "choice", "instructions": "Which team owns this ticket?",
                "criteria": {"infrastructure": "outages, downtime, errors", "billing": "refunds, invoices, charges"}},
      "churn_risk": {"type": "noul", "instructions": "Does the customer threaten to cancel?"}
    }
  }'
```

```python
import offrec
px = offrec.Client()                                   # reads OFFREC_API_KEY (offrec >= 0.5.0)
result = px.decide(
    "Checkout has been down for an hour and we're losing sales. Fix it or we cancel.",
    {
        "queue": {"type": "choice", "instructions": "Which team owns this ticket?",
                  "criteria": {"infrastructure": "outages, downtime, errors",
                               "billing": "refunds, invoices, charges"}},
        "churn_risk": {"type": "noul", "instructions": "Does the customer threaten to cancel?"},
    },
    model="laya",
)
result["answers"]["queue"]["choice"]       # "infrastructure"
```

## 2. Write the questions

A request carries a `state` and up to 32 `questions`:

```json
{
  "state": "Checkout has been down for an hour and we're losing sales. Fix it or we cancel.",
  "questions": {
    "queue": {
      "type": "choice",
      "instructions": "Which team owns this ticket?",
      "criteria": {"infrastructure": "outages, downtime, errors", "billing": "refunds, invoices, charges"}
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is it?",
      "criteria": ["low", "medium", "high", "critical"]
    },
    "churn_risk": {
      "type": "noul",
      "instructions": "Does the customer threaten to cancel?"
    }
  }
}
```

- **`state`** — text, a JSON object, or a list of conversation turns.
- **`choice`** — pick one option. `criteria` maps each option to a short description
  (or is a plain list of option names).
- **`score`** — place the state on an ordered scale. `criteria` is the list of levels,
  lowest first.
- **`noul`** — a yes/no question. Optional `criteria`:
  `{"true": "...", "false": "..."}` to describe what each answer means.

The question ids (`queue`, `urgency`, …) are yours; they come back as the keys of
`answers`.

> **Describe every option.** Laya decides from the descriptions in `criteria`. In
> our test, `"infrastructure": "outages"` routed a checkout outage to billing;
> `"outages, downtime, errors"` routed it to infrastructure.

## 3. Read the answers

```json
{
  "id": "dec_...",
  "object": "decision",
  "model": "laya",
  "answers": {
    "queue": {
      "type": "choice", "choice": "infrastructure",
      "probabilities": {"infrastructure": 0.6602, "billing": 0.3398},
      "answer_confidence": 0.6602
    },
    "urgency": {
      "type": "score", "score": 1.9951,
      "legend": {"0": "low", "1": "medium", "2": "high", "3": "critical"},
      "probabilities": {"0": 0.0723, "1": 0.2956, "2": 0.1969, "3": 0.4352},
      "answer_confidence": 0.4352
    },
    "churn_risk": {"type": "noul", "noul": 0.9157, "answer_confidence": 0.9157}
  },
  "usage": {"input_tokens": 158, "state_tokens": 20, "truncated": false},
  "min_confidence": 0.0
}
```

- **`choice`** — the chosen option; its probability is `answer_confidence`.
- **`score`** — the **expected** level on the scale (1.995 ≈ "high"). The single most
  likely level is the largest entry in `probabilities` (here "critical", 43.5%) —
  `answer_confidence` is that probability, not the probability of the rounded score.
- **`noul`** — the probability of "yes" (0.92 → yes). `answer_confidence` is the
  probability of whichever answer is more likely.
- **`usage.truncated: true`** — the state was longer than the model reads; it answered
  from the part that fit.

## Abstention threshold — route the unsure cases to a person

`min_confidence` (0–0.99, default 0 = off) marks answers whose calibrated confidence
falls below it with `"low_confidence": true`:

```python
result = px.decide(state, questions, min_confidence=0.8)
```

```bash
curl https://api.offrec.ai/v1/decisions ... -d '{"model": "laya", "state": ..., "questions": ..., "min_confidence": 0.8}'
```

## Errors

| Status | When |
|---|---|
| `422` | `state` or `questions` is missing, more than 32 questions, over 256 KB, an unknown question `type`, or a misspelled field. The message names the question at fault. |
| `404` | An unknown `model` (the message lists the real ones). |
| `401` | Missing or wrong key. |
| `402` | The account has no credits (the same gate as deploying a model). |
| `429` | Every Laya slot is busy — retry after the `Retry-After` seconds. |
| `503` | Laya is not available on this server right now. |

## Or deploy it as an endpoint

Deploy Laya when you want what an endpoint adds: per-endpoint `pnp_...` keys for
external callers, per-endpoint stats, default parameters, or a
[data-compliant](/compliance) endpoint with a `compliance` receipt on every response.

**The console:** Deploy → **Open source** → **Laya** → Deploy, then **Keys → Create key**.

```python
dep = px.deployments.deploy_open_source(project["id"], "laya", min_confidence=0.6)
key = px.deployments.create_key(dep["id"])["api_key"]
result = px.deployments.predict(dep["id"], "request.json", api_key=key)
```

The endpoint takes the same request as a JSON **file** (the `file` field of
`POST /v1/deployments/{id}/predict`, catalog ids `laya` / `laya_multilingual`) and
returns the same `answers`:

```bash
curl -X POST https://api.offrec.ai/v1/deployments/DEPLOYMENT_ID/predict \
  -H "X-API-Key: pnp_..." \
  -F "file=@request.json;type=application/json" \
  -F 'params={"min_confidence": 0.8}'
```

## Choosing a version

- **English text:** `laya`. In a small check of ours (8 questions) it also answered
  short Spanish, French and German yes/no questions correctly — too few to promise.
- **Long non-English states:** `laya-multilingual` reads twice as much and routes
  non-English text well, but it was weaker on yes/no questions about *conditional*
  threats ("if you don't fix it, I cancel"). Test it on your own cases first.

Nothing about a decision is stored: a direct call never touches disk, and only
metadata (model, question count, latency) is logged.
