Skip to main content
You’ve got a whole pile of short texts to label, like support tickets, social posts, or product reviews. And you want to assign one category and any number of tags to every item in a single request to the Jev model through the Decisions API at OpenRouter, and run that request over the whole pile concurrently with backoff. You could try to use a chat model for this labeling job, but chat model labeling means writing a prompt, parsing a block of free text, and paying for output tokens on every item. With Jev, a decision model rather than a generator, you don’t need to parse or pay for output tokens at all. Just phrase each label as a question and it’ll tell you how likely it thinks the answer is yes, as a probability from 0 to 1. The following steps will help you build the question set up front, run the whole batch request within rate limits, pick a threshold per tag from a small human-labeled sample, and turn the Jev usage returned in the response into a budget. Here’s a worked example on the public TweetTopic dataset: the captured run came out to $0.014 across 550 tweets. Goal: Define a classifyPost function that returns an exclusive category, its confidence, and a probability per tag for one item, then run it over a batch and choose thresholds and a budget from measured results. Outcome: Your app labels items with a category and a set of tags at a threshold you chose from data, stays inside the provider’s rate limits, and logs cost per 1,000 items from usage.cost.
Prerequisites:
  • Your existing TypeScript app runs on Node.js or Bun as an ES module (step 3 uses top-level await), has @types/node or @types/bun for process and fetch, and keeps the items to classify in an array of strings named posts.
  • You’ve configured OPENROUTER_API_KEY and have zod 4 installed, since this page imports it directly.
  • You’ve read the TypeSafe SDK guide or the Decisions API reference once, so the state and questions request shape is familiar.
  • Keep the rate limits page open for the status codes that step 3 retries.

1. Build the label set as Jev questions

The first thing to do after defining the label set is turn those labels into questions. Jev answers questions about the item you put in state. First is the single choice category question: categories that never overlap go into one choice question that picks exactly one from the list. The rest are tags that may be present or absent. Each of those gets its own noul question that is answered with a probability between 0 and 1. The odds that one tag applies are judged independently of the others. Writing your labels this way lets one request return the category and full tag probabilities at once.
Write each noul instruction as a yes-or-no question about the item and put the category definitions in the choice question’s criteria rather than in its instruction text. Jev reads the item from state, so the question refers to the post and never repeats the text. If your categories might not cover every item, add an other entry to both CATEGORIES and criteria, since a choice always picks one of the options you list and the step 2 schema accepts only values from CATEGORIES. The six TweetTopic categories above cover every item in that dataset, so the captured run omits it.

2. Classify one item with one Decisions request

A per-item classification calls the Decisions endpoint, passes it the post and the questions, checks that its response looks right before trusting it, and returns the category, its confidence, the tag probabilities, and the usage figures. Rate limit, server error, or dropped connection throws a RetryableError so step 3 can back off and try again. Anything else throws a plain error so the item fails instead of repeating. The Decisions schema marks confidence and usage.cost as optional. Missing confidence stays undefined, and step 4 will route those items to review. If there’s no usage.cost, then the response is rejected rather than being counted as free.
Every call to classifyPost is a paid request. At the time of writing, Jev on OpenRouter costs $0.042 per million input tokens with no output-token charge, and the current price is on the model page. One kind of 402 is transient: when your running requests fill the in-flight spending budget, the body includes limit_source: "openrouter_in_flight_budget" and the code retries after Retry-After. Any other 402 means your credits or key limit ran out, so the item fails and runBatch stops the batch.

3. Run the Jev batch concurrently within rate limits

Take a few thousand items and run them a single request at a time and you’ll be waiting minutes. Obviously, you’ll want to run your requests in parallel. You could start them all at once, but you’ll get rate limited. runBatch solves this with a fixed number of workers that take items from the list one at a time. withRetry detects transient errors from step 2 and waits a while so they can get resolved. All workers wait together, and they’ll all resume at the same time when one request is told to slow down. On a terminal failure, other workers stop taking new items and runBatch rejects once all in-flight requests are done. Your results come back in the same order as your input list.
withRetry understands a Retry-After header that comes back in the response. It will use the numeric value as the wait and ignore an HTTP-date value. Otherwise, it’ll back off from 1 second with some jitter until it hits a 30-second cap. Start with 8 workers, which is what the captured run used with no 429s. Raise the worker count while a full batch completes with no 429s, and halve it when retries show up in your logs.

4. Pick tag thresholds from a human-labeled sample

A noul probability only becomes a tag once it crosses a threshold, and the threshold level differs for each tag. Here’s a procedure for learning appropriate thresholds for each tag. Label 100 to 200 representative items by hand, and use runBatch to classify them in the same order that you used when labeling them. Then compute precision and recall for the different tags at each of several candidate thresholds. Raising the threshold increases precision at the expense of recall. If you can afford to miss a few items that should have the tag, set the lowest threshold where you are satisfied with the precision. If you can’t afford to miss the items, set the highest threshold whose recall you can accept.
precisionRecall returns undefined instead of a score when a threshold predicts no positives or when the sample contains no positive examples for a tag. In that case you’ll want to label more items rather than pick a threshold from an undefined score. The choice answer needs no threshold since its confidence tells you which items to hand to a person. Group the sample by confidence band, compare the category with the human label in each band, and send to review anything below the band that meets your accuracy bar. Run this calibration once, separately from the production batch in step 3, since both are paid.
When a tag’s precision stays low at every threshold, fix the question rather than the threshold. In the captured run, celebrity never passed 0.41 precision because its instruction, Is the post about a celebrity or pop culture?, overlaps with music and film_tv. Tighten the instruction or the criteria and re-run the sample before shipping that tag.

5. Compute Jev cost per 1,000 items from returned usage

Budgeting a larger batch means summing up usage.cost for each result, and scaling to 1,000 items, all plain arithmetic on real numbers. And you keep the tokens per item next to the scaled cost so that you can predict the bill for more text of the same shape before actually running it.

Worked example

Captured output. The code above ran unchanged on 2026-09-21 against the test_2021 split of TweetTopic, a public dataset of tweets with human-assigned topic labels (Antypas et al., COLING 2022). The six TAGS map to six of its nineteen multi-label topics, and CATEGORIES map to the six exclusive classes of its single-label companion set. Items 1 to 150 were the calibration sample and items 151 to 550 were the batch, with concurrency of 8. Usernames and URLs appear as {@name@}, {{USERNAME}}, and {{URL}} because the dataset masks them. Below is one batch item with human labels (category pop_culture, tags film_tv and music) and with output reduced to the fields classifyPost returns.
A threshold sweep was performed on the 150-item calibration sample, showing precision/recall at different thresholds, with true positives, false positives, and false negatives as tp/fp/fn in parentheses. sports was clean at any threshold. music traded 12 recall points for 26 precision points between threshold 0.5 and 0.8. news and film_tv could not reach high recall at any threshold with these instructions. celebrity could not reach high precision.
The choice answer matched the human category on 127 of 150 calibration items. Confidence splits here are at 0.8 or above: 114 of 122 matched; at 0.5 to below 0.8: 9 of 18; below 0.5: 4 of 10. A review threshold of 0.8 on confidence would have sent the 28 items below it to a person. On the 400-item batch it got 350 of 400 correct. The cost and timing from costPerThousand on that batch run are shown below. 400 requests took 8.6 seconds at 8 workers with no retries. The whole 550-item run cost $0.0141.
The per-thousand matches list price: 609.9 input tokens per item, at $0.042 per million tokens, would be $0.0000256 per item, or $0.0256 per 1,000 items. output_tokens were not billed. Longer items or more tags raise tokensPerItem, so measure on your own sample before quoting a budget.

Check your work

The following are things to check about the behavior you’ve implemented.
  • Decisions response. answers.category.type equals 'choice' and answers.category.choice is one of the six CATEGORIES. Each tag key has type equal to 'noul' and noul between 0 and 1. usage.cost is a number in USD and usage.output_tokens adds nothing to it.
  • Endpoint and model. Requests reach https://openrouter.ai/api/alpha/decisions with model set to typesafe/jev-1.13, and the response model names a concrete Jev version.
  • Jev batch. runBatch never has more than concurrency requests in flight, a 429, 5xx, or in-flight-budget 402 response pauses every worker and is retried with backoff, and a 400 or any other 402 fails the item without a retry.
  • Threshold tuning. Raising a tag’s threshold in sweepThresholds never raises its tp or fp counts, so recall falls or stays flat while fewer false positives pass.
  • Jev cost. costPerThousand(results).cost equals the sum of usage.cost over the batch and perThousand equals that sum divided by the item count times 1,000.

Next steps

Here are a few things you can try next: