# Themes

> [!NOTE] **Not enabled yet.** AI enrichment ships when a production model is connected; every number on this page comes from the pre-release test provider and will change. It is switched on per account, so nothing here describes real-world judgment quality yet. Where it is not enabled, a request that asks for it with the default `strict=true` is refused with `503 AI_UNAVAILABLE` at no charge; with `strict=false` you get the rows back unjudged. Enrichment is also switched on per platform: only the endpoints in the [table on the Labels page](https://onekey.dev/docs/labels.md#which-endpoints-accept-it) accept these parameters.

`themes=true` groups the rows a call returned by what they talk about and gives each group a name, a size, the ids of its rows and a sample. Ninety-seven replies to a post become a handful of topics you can read in a minute.

## When to use it

- You have a long comment or reply list and want to see the main topics before reading individual rows.
- You want counts per topic, with the row ids to jump back to the originals.

It works on the same 31 list endpoints as [labels](https://onekey.dev/docs/labels.md#which-endpoints-accept-it), and on [multi-platform search](https://onekey.dev/docs/search-multi.md).

## The request

```bash
curl "https://api.onekey.dev/v1/bluesky/post_replies?uri=at://did:plc:…/app.bsky.feed.post/…&themes=true" \
  -H "x-api-key: YOUR_API_KEY"
```

```python
import requests

res = requests.get(
    "https://api.onekey.dev/v1/hackernews/item_comments",
    params={"id": "8863", "themes": "true"},
    headers={"x-api-key": "YOUR_API_KEY"},
)
body = res.json()

if body["success"]:
    for theme in body["themes"]["themes"]:
        print(theme["size"], theme["theme"])
else:
    print(body["error"]["type"], body["error"]["message"])
```

```javascript
const params = new URLSearchParams({ id: "8863", themes: "true" });
const res = await fetch(`https://api.onekey.dev/v1/hackernews/item_comments?${params}`, {
  headers: { "x-api-key": "YOUR_API_KEY" },
});
const body = await res.json();

if (body.success) for (const t of body.themes.themes) console.log(t.size, t.theme);
else console.error(body.error.type, body.error.message);
```

| Parameter | Values | What it does |
| --- | --- | --- |
| `themes` | `true` | Cluster the rows the call returns and name each cluster. |
| `strict` | `true` (default) or `false` | As on [Labels](https://onekey.dev/docs/labels.md#when-nothing-could-be-judged). Only matters for the naming step. |

Combine it with `relevance=filter` to cluster only the rows that survived the filter.

## How it works

1. **Clustering is deterministic and involves no model.** The rows' text is compared with a standard word-frequency similarity (TF-IDF cosine) and grouped. The same rows always give the same clusters, and there are at most 12.
2. **Only the names come from the provider.** One call names all the clusters at once. If enrichment is off or the naming fails, the clusters are still returned, named `Theme 1`, `Theme 2` and so on, with `named: false`.

## The response

The `themes` block sits at the top level next to `data`. Recorded on the pre-release test provider (list shortened):

```json
{
  "success": true,
  "data": [ "…97 replies…" ],
  "themes": {
    "status": "complete",
    "rows": 97,
    "clustered": 92,
    "skipped": 5,
    "named": true,
    "extra_credits": 7.942,
    "methodology_version": "fake/1",
    "clustering": "cluster/1",
    "themes": [
      { "theme": "Bsky Bluesky Social", "size": 41, "row_ids": ["at://did:plc:…/3mw2eruoksc2x", "…"], "sample": "…", "terms": ["bsky", "bluesky", "…"] }
    ]
  },
  "metadata": { "credits_charged": 12.942, "extra_credits": 7.942, "…": "…" },
  "error": null
}
```

| Field | Meaning |
| --- | --- |
| `rows`, `clustered`, `skipped` | Rows returned, rows placed in a theme, and rows with no usable text (which are in no theme). |
| `named` | `true` when a provider named the clusters; `false` when they carry the placeholder names. |
| `themes[].theme` | The cluster's name. |
| `themes[].size` | Rows in the cluster. |
| `themes[].row_ids` | The rows' ids (the record's own id, or `row-<index>`). On multi-platform search they are `platform:id`. |
| `themes[].sample` | A representative text from the cluster. |
| `themes[].terms` | The words that define the cluster. These come from the clustering, not from a model. |
| `clustering` | The clustering method, `cluster/1`. |
| `methodology_version`, `status`, `extra_credits` | As on [Labels](https://onekey.dev/docs/labels.md#the-response). |

## How to read it honestly

- **Themes are a reading aid, not a taxonomy.** Grouping is by shared words. It works best on short comments; it can merge long texts that share vocabulary, and it can split paraphrases that share no words.
- **A name is a summary of a sample, not a finding.** Check `terms` and `sample` before you quote a theme's name.
- **Every row is accounted for.** `skipped` rows are in no theme, so `clustered` plus `skipped` is `rows`.
- **Not deduplicated across platforms.** On multi-platform search, the same URL posted on two platforms is two rows, and clustering runs once over the kept rows of every platform that accepts enrichment.

## Multi-platform search

On [`/v1/search/multi`](https://onekey.dev/docs/search-multi.md), `data.themes` is computed once over the kept rows of every opted-in platform (after `relevance=filter`, if used), and a stream carries it in an `enrich` frame. See [Streaming](https://onekey.dev/docs/streaming.md).

## What it costs

One separate charge for the naming call, on top of the endpoint's price. A call with the provider off returns the placeholder-named clusters at no extra charge. See [AI pricing](https://onekey.dev/docs/ai-pricing.md).

## Errors it can return

| Status | Type | When |
| --- | --- | --- |
| 422 | [`INVALID_PARAMS`](https://onekey.dev/docs/errors.md#invalid-params) | `themes` on an endpoint that does not accept enrichment. Free. |
| 429 | [`AI_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#ai-budget-exceeded) | Today's allowance is used up and `strict=true`. |
| 503 | [`AI_UNAVAILABLE`](https://onekey.dev/docs/errors.md#ai-unavailable) | Enrichment is off or unavailable and `strict=true`. |
| 402 | [`INSUFFICIENT_CREDITS`](https://onekey.dev/docs/errors.md#insufficient-credits), [`KEY_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#key-budget-exceeded) | The reservation cannot be covered. |

See [Errors](https://onekey.dev/docs/errors.md) for the full list.
