# Rubric

> Writes a chat's title and a one-line preview of an email, message, ticket or agent action. Send the item as JSON and get one line back, at $0.10 per million input tokens with output free.

Rubric is a small model that writes two things: a **title** for a chat, in 2 to 7 words, and a **preview** of an item in a list, in at most 15 words, telling the reader what they would learn by opening it.

You send the chat or the item as data, not a prompt. The control plane renders it exactly as the model was trained on, asks the model, checks the line that comes back, and returns it.

```bash
curl https://api.divergentlabs.xyz/api/inference/rubric/v1/titles \
  -H "Authorization: Bearer $DIVERGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "why does my docker container exit immediately?"}]}'
```

```json
{
  "object": "title",
  "model": "rubric",
  "text": "Docker container exiting immediately",
  "fallback": false,
  "usage": { "input_tokens": 45, "output_tokens": 6 },
  "latency_ms": 363
}
```

The key needs the `inference:invoke` scope. Keys made by [`div login`](https://docs.divergentlabs.xyz/compute/cli/login.md) don't have it; make one under Settings in the dashboard. To try a request before writing code, use **Playground** in the dashboard. Its curl changes as you edit the form.

## Titles

`POST /api/inference/rubric/v1/titles`

| Field | |
| --- | --- |
| `messages` | the chat, oldest first. Each message has `content` and a `role` (`user`, `assistant`) or a `name`, or both. |

A long chat is cut down before the model reads it: each message to its first 4,200 and last 1,800 characters, and the chat to about 12,000 characters. Messages past that are counted instead of included.

## Previews

`POST /api/inference/rubric/v1/previews`

```bash
curl https://api.divergentlabs.xyz/api/inference/rubric/v1/previews \
  -H "Authorization: Bearer $DIVERGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "email",
    "reader": "Sam Haddad",
    "item": {
      "from": "Ines Okafor",
      "to": "Sam Haddad",
      "subject": "Re: coffee",
      "body": "Hey Sam! Want to come to the Harbour Street café instead? We could work from there afterwards."
    }
  }'
```

```json
{
  "object": "preview",
  "model": "rubric",
  "text": "Ines suggests meeting at the Harbour Street café to work afterwards.",
  "fallback": false,
  "usage": { "input_tokens": 93, "output_tokens": 14 },
  "latency_ms": 518
}
```

| Field | |
| --- | --- |
| `kind` | `email`, `message`, `ticket`, `issue`, `event`, `doc` or `agent_action` |
| `reader` | who is looking at the list. Their name is marked "(you)" wherever it appears, so a message they sent comes back as "You offered to…". Optional; without it, nobody in the item is "you". |
| `item` | the item, below |

Every `item` field is a string, and all of them are optional:

| Field | |
| --- | --- |
| `subject`, `title`, `channel`, `status`, `when`, `where` | headers, shown in this order |
| `from`, `to`, `cc` | names, comma-separated. The reader's name is marked in each. |
| `date` | as you would show it |
| `body` | the newest message. Its first 8,400 and last 3,600 characters are kept. |
| `thread` | the messages before the newest, in the shape `messages` takes for a title. Only the newest is summarized; the thread is context. |

An `agent_action` is a tool call, and takes different fields:

| Field | |
| --- | --- |
| `tool` | the tool's name. Required. |
| `arguments` | an object, or a string |
| `result` | what the tool returned, as a string. Leave it out or send `null` while the tool is still running, and the preview reads "Searching the web for…". |
| `context` | what the agent was doing, if you have it |

Previews are in English, whatever language the item is in. `language` is accepted only as `"English"`.

## The answer

`text` is one line, with no quotes and no label. Before it is returned it is checked: a title must be 2 to 8 words with no quotes, no emoji and no full stop at the end; a preview must be 1 to 20 words and at most 160 characters; both must be English and on one line.

A line that fails a check is not returned. `text` is a fallback built from the input instead, and `fallback` is `true`:

| Request | Fallback |
| --- | --- |
| a title | `New conversation` |
| an `agent_action` | `Running <tool>…`, or `Ran <tool>` once there is a result |
| any other preview | the subject, else the title, else the body's first line, cut at 15 words |

The request is billed either way, since the model read the input either way.

## Price

$0.10 per million input tokens. Output is free.

`usage` is the tokens the model reports for the request. Nothing is charged per request. The meter adds up each org's tokens for each UTC day, prices the day, and posts what has not been charged yet, so the day is rounded up to a cent once rather than on every request. The charges show on the Billing page by model. Each request shows under **Endpoints → Requests**, with the key that made it. `latency_ms` is the model's time for the request, the same figure that list shows, so it leaves out the network between us and you.

A request needs credit. An org at zero gets a 402 before the model is asked.

## Errors

Errors have the same shape as a chat completion's, with `code` to branch on and `message` for a person to read:

```json
{ "error": { "code": "invalid_request", "type": "invalid_request_error", "message": "item.tool: required, as a string", "param": "item.tool" } }
```

| Status | `code` | When |
| --- | --- | --- |
| 401 | `unauthorized` | no key, or a key that has been revoked |
| 402 | `insufficient_credit` | the org's credit is used up |
| 403 | `insufficient_scope` | the key lacks `inference:invoke` |
| 404 | `model_not_found` | the path names a model that does not write titles and previews |
| 422 | `invalid_request` | a field is missing or the wrong type. `param` is its path, such as `messages[0].content`. |
| 502 | `upstream_unavailable` | the model did not answer. Nothing was charged. |
| 503 | `not_configured` | the model is not deployed where this control plane can reach it |
