# Labels

> [!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.

`include=labels` and `label=` add small, structured judgments to every row a list endpoint returns: is this comment a question, is it a complaint, how positive is it, is the person asking to buy. You keep the endpoint's normal answer and get the judgments next to each row, in `computed.labels`.

Labels are **signals, not verdicts**. Each one is a level or a probability with a score, and you pick your own cut-off. Nothing else in https://api.onekey.dev is produced by a language model; this feature, [relevance](https://onekey.dev/docs/relevance.md) and [themes](https://onekey.dev/docs/themes.md) are the only places one is involved, and the response says which method produced them.

## When to use it

- You pull the comments or replies under a post and want to find the questions, the complaints and the buying intent without reading them all.
- You want a sentiment number per row to sort or chart, and you are happy to choose the threshold yourself.
- You can live with a judgment being missing on a row: a row that could not be judged says so with `null` and is never guessed.

Labels are extra work on top of the endpoint, so they are metered separately. See [AI pricing](https://onekey.dev/docs/ai-pricing.md).

## The request

Add `include=labels` to any [endpoint that accepts it](#which-endpoints-accept-it). This judges every comment under a Hacker News story with that endpoint's default presets:

```bash
curl "https://api.onekey.dev/v1/hackernews/item_comments?id=8863&include=labels" \
  -H "x-api-key: YOUR_API_KEY"
```

```python
import requests

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

if body["success"]:
    summary = body["labels"]
    print(summary["status"], summary["labelled"], "of", summary["rows"], "rows judged")
    for row in body["data"]:
        labels = row["computed"]["labels"]
        if labels and labels["question"] and labels["question"]["p"] >= 0.7:
            print(row["content"]["text"][:80])
else:
    print(body["error"]["type"], body["error"]["message"])
```

```javascript
const params = new URLSearchParams({ id: "8863", include: "labels" });
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) {
  const { status, labelled, rows } = body.labels;
  console.log(status, labelled, "of", rows, "rows judged");
} else console.error(body.error.type, body.error.message);
```

To choose the presets yourself, name them with `label=`. It implies `include=labels`, and fewer presets cost less:

```bash
curl "https://api.onekey.dev/v1/hackernews/item_comments?id=8863&label=question,complaint" \
  -H "x-api-key: YOUR_API_KEY"
```

| Parameter | Values | What it does |
| --- | --- | --- |
| `include=labels` | flag | Judge every row with the endpoint's default presets (below). |
| `label=sentiment,question` | comma list, at most 5 | Judge exactly these presets. Implies `include=labels`. An unknown name is a free `422 INVALID_LABEL`. |
| `strict=true` | `true` (default) or `false` | What happens when **nothing at all** could be judged. See [When nothing could be judged](#when-nothing-could-be-judged). |

These combine with the other enrichment parameters on the same call: [`relevance=`](https://onekey.dev/docs/relevance.md) and [`themes=`](https://onekey.dev/docs/themes.md).

## The presets

| Preset | Value per row | Reads as |
| --- | --- | --- |
| `sentiment` | `{level, score_0_1, confidence}` | `level` is 1 (very negative) to 5 (very positive); `score_0_1` is the same on a 0 to 1 scale. |
| `question` | `{p}` | Probability the text is asking a question. |
| `purchase_intent` | `{p}` | Probability the person is looking to buy or asking where to buy. |
| `complaint` | `{p}` | Probability the text is a complaint. |
| `issue` | `{topic, confidence}` | The main problem raised. `topic` is one of `shipping`, `pricing`, `billing`, `quality`, `support`, `usability`, `performance`, `content`, `account`, `other` or `none`. |

`p` and `confidence` are numbers from 0 to 1. There is never a bare `is_complaint: true`: you decide where the line is.

### Defaults by row kind

Without `label=`, the presets depend on what a row is:

| Rows are | Default presets |
| --- | --- |
| Comments | `sentiment`, `question`, `complaint`, `issue` |
| Posts | `sentiment`, `question`, `purchase_intent` |
| Reviews | `sentiment`, `complaint`, `issue`, `purchase_intent` |
| Search results | `sentiment`, `question` |

## Which endpoints accept it

31 list endpoints on 11 platforms accept enrichment today, plus [multi-platform search](https://onekey.dev/docs/search-multi.md) (per platform) and [batch](https://onekey.dev/docs/batch.md) items, which take the same parameters as a single call. The table is generated from each endpoint's own declaration, so it is always current; `features.ai` in `GET /v1/platforms` and `x-ai` in [`/openapi.json`](https://onekey.dev/openapi.json) mark the same endpoints. The judged text is the field named in the third column: for a search result that is the title, for a post the title and body together.

| Endpoint | Rows are | Judged text | Default presets | `relevance` |
| --- | --- | --- | --- | --- |
| [`bluesky.post_replies`](https://onekey.dev/docs/api/bluesky/post_replies.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`bluesky.posts`](https://onekey.dev/docs/api/bluesky/posts.md) | Posts | `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`bluesky.search`](https://onekey.dev/docs/api/bluesky/search.md) | Search results | `content.text` | `sentiment`, `question` | yes |
| [`devto.articles_by_tag`](https://onekey.dev/docs/api/devto/articles_by_tag.md) | Posts | `content.title` + `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`devto.comments`](https://onekey.dev/docs/api/devto/comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`devto.search`](https://onekey.dev/docs/api/devto/search.md) | Search results | `content.title` + `content.text` | `sentiment`, `question` | yes |
| [`github.issue_comments`](https://onekey.dev/docs/api/github/issue_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`github.issues`](https://onekey.dev/docs/api/github/issues.md) | Posts | `content.title` + `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`github.search_issues`](https://onekey.dev/docs/api/github/search_issues.md) | Posts | `content.title` + `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`gitlab.issues`](https://onekey.dev/docs/api/gitlab/issues.md) | Posts | `content.title` + `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`gitlab.search`](https://onekey.dev/docs/api/gitlab/search.md) | Search results | `content.title` + `content.text` | `sentiment`, `question` | yes |
| [`hackernews.author_comments`](https://onekey.dev/docs/api/hackernews/author_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`hackernews.item_comments`](https://onekey.dev/docs/api/hackernews/item_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`hackernews.search`](https://onekey.dev/docs/api/hackernews/search.md) | Search results | `content.title` | `sentiment`, `question` | yes |
| [`hackernews.search_comments`](https://onekey.dev/docs/api/hackernews/search_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`hackernews.story_comments`](https://onekey.dev/docs/api/hackernews/story_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`lemmy.comments`](https://onekey.dev/docs/api/lemmy/comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`lemmy.search_comments`](https://onekey.dev/docs/api/lemmy/search_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`lemmy.search_posts`](https://onekey.dev/docs/api/lemmy/search_posts.md) | Posts | `content.title` + `content.text` | `sentiment`, `question`, `purchase_intent` | yes |
| [`lobsters.story_comments`](https://onekey.dev/docs/api/lobsters/story_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`mastodon.status_context_replies`](https://onekey.dev/docs/api/mastodon/status_context_replies.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`mastodon.timeline_tag`](https://onekey.dev/docs/api/mastodon/timeline_tag.md) | Posts | `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`reddit.post_comments`](https://onekey.dev/docs/api/reddit/post_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`reddit.search_posts`](https://onekey.dev/docs/api/reddit/search_posts.md) | Posts | `content.title` + `content.text` | `sentiment`, `question`, `purchase_intent` | yes |
| [`reddit.user_comments`](https://onekey.dev/docs/api/reddit/user_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`stackexchange.answers`](https://onekey.dev/docs/api/stackexchange/answers.md) | Posts | `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`stackexchange.comments`](https://onekey.dev/docs/api/stackexchange/comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`stackexchange.questions`](https://onekey.dev/docs/api/stackexchange/questions.md) | Posts | `content.title` + `content.text` | `sentiment`, `question`, `purchase_intent` | no |
| [`stackexchange.search`](https://onekey.dev/docs/api/stackexchange/search.md) | Search results | `content.title` | `sentiment`, `question` | yes |
| [`youtube.comments`](https://onekey.dev/docs/api/youtube/comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |
| [`youtube.video_comments`](https://onekey.dev/docs/api/youtube/video_comments.md) | Comments | `content.text` | `sentiment`, `question`, `complaint`, `issue` | no |

Any enrichment parameter on another endpoint is a free `422 INVALID_PARAMS`, and so is one on `/v1/creator/card`. The last column says whether the endpoint is a search, where [`relevance=`](https://onekey.dev/docs/relevance.md) is also allowed.

## The response

Each row gets `computed.labels`, and one `labels` summary block sits at the top level next to `data`. This is a recorded call on the pre-release test provider (rows shortened; `methodology_version` carries the test provider's method id, which marks that provider, see below):

```json
{
  "success": true,
  "data": [
    {
      "content": { "id": "8869", "text": "This has great potential! Only suggestion I would have is go slower on the demo…" },
      "author": { "username": "…" },
      "computed": {
        "labels": {
          "complaint": { "p": 0.05 },
          "issue": { "topic": "usability", "confidence": 0.5 },
          "question": { "p": 0.05 },
          "sentiment": { "level": 4, "score_0_1": 0.7, "confidence": 0.95 }
        }
      }
    }
  ],
  "labels": {
    "presets": ["complaint", "issue", "question", "sentiment"],
    "mode": "sync",
    "status": "complete",
    "rows": 32,
    "labelled": 32,
    "cached": 0,
    "skipped": 0,
    "unjudged": 0,
    "pending": 0,
    "dropped": 0,
    "extra_credits": 60.225,
    "methodology_version": "fake/1"
  },
  "metadata": { "platform": "hackernews", "endpoint": "item_comments", "cached": false, "credits_charged": 65.225, "extra_credits": 60.225, "…": "…" },
  "error": null
}
```

| Field | Meaning |
| --- | --- |
| `presets` | The presets that were judged, in the order returned. |
| `mode` | Always `sync` today: the call waits for the judgments. |
| `status` | `complete` (every row that could be judged was), `partial` (some were not) or `unavailable` (none were). |
| `rows` | How many rows the endpoint returned. |
| `labelled` | Rows that have a judgment, whether it was fresh or reused. |
| `cached` | Of those, judgments reused from an earlier identical text. They cost nothing. |
| `skipped` | Rows with nothing to judge: no text, not text, or shorter than 3 characters. Their `computed.labels` is `null`. |
| `unjudged` | Rows that had text but were not judged, for the `reason` given. Their presets are `null`. |
| `pending` | Always `0`. Reserved for a future asynchronous mode. |
| `dropped` | Always `0` for labels. Only [relevance filtering](https://onekey.dev/docs/relevance.md) drops rows. |
| `extra_credits` | What this step cost, on top of the endpoint's price. See [AI pricing](https://onekey.dev/docs/ai-pricing.md). |
| `methodology_version` | Which method produced the judgments. |
| `reason` | Only when `status` is not `complete`. One of `request_cap`, `account_cap`, `global_cap`, `kill_switch`, `provider_off`, `provider_unavailable`, `invalid_output`. |

`metadata.extra_credits` is the sum of every enrichment step in the call, and `metadata.credits_charged` already includes it.

## How to read it honestly

- **Signals, not verdicts.** A `question.p` of 0.7 is a probability from a model, not a fact about the comment. Choose the cut-off that fits your use, and spot-check rows near it.
- **`null` means not judged, never "no".** A skipped row has `computed.labels: null`. A row that had text but was not judged has `{preset: null}` for each preset. Treat both as unknown, not as a negative. Nothing is guessed to fill a gap.
- **Count what was judged.** `rows`, `labelled`, `skipped` and `unjudged` always add up the story. If `labelled` is smaller than `rows`, say so in whatever you build on top.
- **Same text, same judgment.** Identical texts (ignoring case and extra whitespace) are judged once and share the result. Texts longer than 2,000 characters are clipped before judging.
- **`methodology_version` names the method.** It is on every enrichment block. The value in the examples on these pages is the test provider's method id, the pre-release test provider: a deterministic keyword heuristic that exists so the feature can be built and tested end to end. It is **not** a measure of quality, and it is wrong on sarcasm, negation and non-English text. The value will change when a production method is in use, and judgments made under one version are never served as another.
- **Limits per request.** At most 200 rows of one request are judged; the rest stay `null` with `reason: request_cap` and `status: partial`, and you are charged only for the rows judged. Per-account daily allowances also apply; see [AI pricing](https://onekey.dev/docs/ai-pricing.md#limits).

## When nothing could be judged

`strict` decides what happens when not a single row could be judged, because enrichment is off for the account, the provider is unavailable or a daily allowance is used up.

- `strict=true` (the default): the call fails, so you never mistake unjudged rows for judged ones. It is [`AI_UNAVAILABLE`](https://onekey.dev/docs/errors.md#ai-unavailable) (`503`) or [`AI_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#ai-budget-exceeded) (`429`). When enrichment is not enabled at all, this is refused before the data is fetched, so it costs nothing.
- `strict=false`: you get the rows back, `computed.labels` is `null` on each, `labels.status` is `unavailable` with a `reason`, and there is no extra charge. Use it when the rows matter more than the judgments.

A **partial** result is never an error, whichever you choose: you get what was judged, `status: partial` and the reason.

## Batch, multi-platform search and streams

- **Batch.** Put the parameters in an item's `params`. The summary blocks sit on that item's row, not at the top of the response.
- **Multi-platform search.** `include=labels` and `label=` on [`/v1/search/multi`](https://onekey.dev/docs/search-multi.md) apply to every platform whose search endpoint accepts them. Each platform's row carries its own `labels` block and `extra_credits`, and `data.labels` adds them up (`legs`, the counters, and `unsupported`: the platforms whose search has no enrichment, whose rows come back untouched). `data.labels.status` is `complete` only when every platform is.
- **Streams.** With `Accept: text/event-stream` the platform frames arrive first, without the enrichment lines in their `credits_charged`, then `fused`, then one `enrich` frame per step, then `done` with the full total. See [Streaming](https://onekey.dev/docs/streaming.md).
- **Idempotency.** A replay with the same [`Idempotency-Key`](https://onekey.dev/docs/idempotency.md) returns the stored body, labels included, at 0 credits. Changing any enrichment parameter makes it a different request ([`IDEMPOTENCY_KEY_REUSED`](https://onekey.dev/docs/errors.md#idempotency-key-reused)).

## What it costs

The endpoint's own price, plus a separate charge for each step that judged something. It depends on how many rows are judged and how many presets you ask for, and it is reserved before judging and settled to what was actually used. Cached judgments, skipped rows and unjudged rows are free. Details and the measured examples are on [AI pricing](https://onekey.dev/docs/ai-pricing.md).

## Errors it can return

| Status | Type | When |
| --- | --- | --- |
| 422 | [`INVALID_LABEL`](https://onekey.dev/docs/errors.md#invalid-label) | `label=` named a preset that does not exist. Free. |
| 422 | [`INVALID_PARAMS`](https://onekey.dev/docs/errors.md#invalid-params) | An enrichment parameter on an endpoint that does not accept it, or a malformed `include`. Free. |
| 429 | [`AI_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#ai-budget-exceeded) | Nothing could be judged because today's allowance is used up and `strict=true`. Wait `Retry-After`. |
| 503 | [`AI_UNAVAILABLE`](https://onekey.dev/docs/errors.md#ai-unavailable) | Nothing could be judged because 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 for the judgments cannot be covered. |

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