---
name: strom
description: >
  Strom by Uprelic is a decision API: send text, JSON or up to 8 images with named questions, and get typed
  answers (yes/no probability, one choice from your options, or a score on your rubric) with calibrated
  probabilities, in roughly 100–500 ms. Use when code needs a judgment that rules or regexes can't make
  (routing, triage, tagging, moderation, guardrails, checking an LLM's output, image checks), when an LLM
  prompt-and-parse step only picks from known options, or when every row of a dataset needs scoring.
  Also has decision harnesses: extracting fields from PDFs and images, checking an AI agent's tool calls
  and replies against rules, and finding a described object in every frame of a video. Not for
  generating text.
---

# Strom

Strom is an API by Uprelic (Berlin) at `https://api.uprelic.com`. You send content (the **state**) and
named **questions**; Strom answers each one with a typed value and a probability. It never writes text:
it scores the options you define, so it can't break the schema or invent an option.

- **Answer types:** `noul` (probability of yes), `choice` (one of your named options, with
  probabilities for all), `score` (a position on your ordered rubric, with probabilities per level),
  `box` (beta: where an object is in an image, as its edges in fractions of the image).
- **Input:** text or JSON, plus up to 8 images per request. Up to 256 questions per request, all
  answered on the same input, and up to 32,000 input tokens per request.
- **Models:** `strom-fast` (`strom-1.0.7`), fast and calibrated, the default choice; `strom-base`
  (`strom-base-1.0`), in beta: strom-fast that reasons before it answers, a few seconds per question,
  pricier, probabilities not calibrated.
- **Price:** $0.042 per 1M input tokens with `strom-fast`, $0.24 with `strom-base`. Output tokens are
  free. A typical `strom-fast` request costs a thousandth of a cent. Usage costs the same on every plan:
  Free, Starter (€135 a month), Growth (€485) or Enterprise; paid plans add seats, longer request logs, zero
  data retention, agent guard sessions and self-improvement.
- **Speed:** ~90 ms model time for one question on a short text, ~180 ms on 2,000 tokens, plus your
  network round trip.
- **Calibrated (`strom-fast`):** on held-out data, answers given at 90% are right about 90% of the time.
- **Data:** runs on GPU servers in the EU (Gravelines, France). No request goes to a third-party AI provider, except with the optional self-improvement of `/v1/extract`, which you turn on yourself and which uses an EU-hosted AI provider.
  Request contents are deleted after 7 days on Free (30, 180 or 365 on paid plans) and never used for training.
- **Access:** sign up at https://platform.uprelic.com and create an API key in the console. Verifying a
  card on the console's Overview adds 5 units of credit in the account's currency (USD, EUR, GBP or CHF),
  once per card; the card isn't charged.

## When a decision model fits

A decision model like Strom doesn't generate text. It reads the input once and answers each question
with a single token: the probabilities of the options you defined. That's why it's fast and cheap,
why every answer fits your schema, and why the probabilities are meaningful enough to act on.

**How it compares:**

- **Rules and regexes** are free and exact, but break on wording nobody anticipated. Strom handles
  the phrasing, language and context a rule can't, from a question in plain language.
- **An LLM with structured output** can reason step by step and explain itself, but it's slower and
  costlier per decision, can still break the schema, and its stated confidence isn't calibrated.
- **A fine-tuned classifier** can beat a general model on one narrow, stable task, but needs labeled
  data and retraining whenever a label changes. With Strom, a new label is a new line in `criteria`.

**It fits when:**

- The possible answers can be written down: yes/no, one of a set, a level on a scale.
- Everything needed to decide is in the state (the text, record fields, images, the policy to check).
- A person would decide it in a few seconds, without research or long calculation.
- The volume or latency matters: every ticket, every row, every video frame, a user-facing feature,
  or an agent acting step by step.

**It doesn't fit when:**

- The answer has to be written: replies, summaries, plans, code, or extracting values you can't list
  as candidates.
- The decision needs multi-step reasoning, arithmetic, or facts that aren't in the state.
- You need an explanation of why, not just the answer and its probability.
- The state, images and a question don't fit in 32,768 tokens.
- Plain code is exact: thresholds, lookups, date math, known rules.
- There are only a handful of decisions, so an LLM's cost and latency don't matter.

Most real systems combine them: Strom triages, an LLM drafts the one step that needs words, Strom
checks the draft against policy, and code decides whether to send it or ask a person. Examples of
Strom's part: which team, how urgent, is it spam, does this draft follow policy, is the right size
selected, did the grasp succeed, which element to click next.

## Switching from Jev

Strom accepts the same request format as TypeSafe's Jev (state plus typed questions). Switching means
pointing the client at `https://api.uprelic.com` with an Uprelic key.

## First request

```sh
curl https://api.uprelic.com/v1/systemone \
  -H "Authorization: Bearer $UPRELIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "strom-1.0.7",
    "state": "Our whole team is blocked. The app has been down since this morning.",
    "questions": {
      "team": {
        "type": "choice",
        "instructions": "Which team should handle this?",
        "criteria": {
          "engineering": "Outages and bugs",
          "billing": "Payments and invoices",
          "account": "Login and access"
        }
      },
      "urgent": {"type": "noul", "instructions": "This message needs urgent attention."}
    }
  }'
```

```json
{
  "model": "strom-1.0.7",
  "answers": {
    "team": {
      "type": "choice",
      "choice": "engineering",
      "confidence": 0.83,
      "probabilities": {"engineering": 0.89, "billing": 0.07, "account": 0.04}
    },
    "urgent": {"type": "noul", "noul": 0.96}
  },
  "usage": {"input_tokens": 283, "output_tokens": 2, "cached_input_tokens": 256, "cost": 0.000011886,
            "currency": "usd"},
  "metadata": {"server_processing_time_ms": 85}
}
```

The values above are illustrative.

### Python SDK

`pip install uprelic` (Python 3.10+). The client reads `UPRELIC_API_KEY`.

```python
from uprelic import Uprelic, Noul, Choice, Score

with Uprelic() as client:
    response = client.system_one(
        model="strom-1.0.7",
        state="I was charged twice for my subscription. Please fix this today.",
        questions={
            "billing": Noul("Is this message about billing?"),
            "tone": Choice("What is the tone?", criteria={"angry": "Upset or hostile", "calm": "Neutral or polite"}),
            "urgency": Score("How urgent is this?", criteria=["Can wait", "This week", "Today"]),
        },
    )

response.nouls["billing"].noul        # 0.98
response.choices["tone"].choice       # "angry"
response.scores["urgency"].score      # 1.8
response.usage.input_tokens
```

Images go in `images=` as http(s) or `data:image/...` URLs. `AsyncUprelic` has the same interface.
`client.extract(model=..., schema={...}, document=Path("invoice.pdf"))` calls `/v1/extract` (see Document
extraction); `document` can also be bytes or a URL. `client.video_detection(description=..., video=url)` starts a
video detection job, waits for it and returns it (see Video detection).
Errors raise `uprelic.APIError` subclasses: `AuthenticationError` (401), `InsufficientBalanceError`
(402), `InvalidRequestError` (400, 413, 422), `RateLimitError` (429), `ServerError` (5xx). 429, 502–504
and connection failures are retried twice with backoff (`Uprelic(max_retries=...)`).

There is no JavaScript SDK; use `fetch` against the HTTP API.

## API reference

- `POST https://api.uprelic.com/v1/systemone` with `Authorization: Bearer <API_KEY>`.
- `POST https://api.uprelic.com/v1/extract` extracts fields from a PDF or image; see Decision harnesses
  below.
- `POST https://api.uprelic.com/v1/harness/video-detection` finds a described object in every frame of a video; see
  Decision harnesses below.
- `GET https://api.uprelic.com/v1/models` lists models; no key needed: `strom-1.0.7` (alias
  `strom-fast`) and `strom-base-1.0` (alias `strom-base`, beta).
- Full schema: https://api.uprelic.com/v1/openapi.json, rendered at https://api.uprelic.com/v1/docs.

### Request

| Field | Type | Meaning |
| --- | --- | --- |
| `model` | string | `strom-1.0.7` or `strom-fast`; `strom-base-1.0` or `strom-base` (beta) |
| `state` | string, object or array | The content every question refers to |
| `questions` | object | 1–256 questions, keyed by names you choose; answers use the same keys |
| `media` | array, optional | Up to 8 `{"type": "image", "url": ...}`, in order |

Question types:

| `type` | `instructions` | `criteria` | Answer |
| --- | --- | --- | --- |
| `noul` | The yes/no question or statement | Optional `{"true": ..., "false": ...}` describing each side | `noul`: probability of yes, 0–1 |
| `choice` | What to decide | Object of option name → description (or `null` to go by the name alone), 1–256 options | `choice`, `confidence`, `probabilities` per option |
| `score` | What to rate | Ordered list of level descriptions; the first is 0 | `score` (probability-weighted level, can be fractional), `confidence`, `legend`, `probabilities` per level |
| `box` (beta) | The object to find, like `the white SUV` | None; instead `image`: the image's position in `media`, from 0, and optionally `multiple: true` to find every match | `box` (`left`, `top`, `right`, `bottom` as fractions of the image's width and height, left < right, top < bottom), `confidence`; with `multiple`, `boxes` instead: every match at least 50% likely, each `{box, probability}`, most likely first, empty when none |

`instructions` and criteria descriptions can be strings or JSON objects. Image URLs are
`data:image/...` or public http(s) URLs (max 10 MB, 10 s, no redirects). Images are scaled to at most
1 megapixel.

A `box` question also sees the request's other images, in their order in `media`, so its instructions
can compare them: with a car's pickup photo at 0 and its return photo at 1, `{"type": "box", "image": 1,
"multiple": true, "instructions": "damage that isn't in the first image"}` finds new damage.

### Cost and limits

- Input tokens = the state once + each question's text + each image's tokens. An image costs one
  token per 32×32 pixels, between 64 and 1,024. A `box` question costs as much as one question plus
  a copy of its image for each region it considers (at most 8), or with `multiple`, one question per region. The
  images themselves are paid once, only when other questions or a `box` question on another image see them. Every response reports `usage.input_tokens`.
- Only answered requests (200) are charged, from a prepaid balance. An empty balance returns 402.
- Questions in progress at once: 512 per account on `strom-fast` (64 on `strom-base`), and the
  shared capacity of all accounts. Over either, 429 with `Retry-After`. Higher limits via sales@uprelic.com.
- Errors: 400 (bad image), 401 (missing or wrong key), 402, 413 (over 32,000 input tokens), 422 (invalid
  request, with the field path),
  429, 5xx. Error bodies have a `detail` field.

### Latency

Model time, median, one request at a time, measured on the production API:

| Input | 1 question | 8 questions | 16 questions |
| --- | --- | --- | --- |
| 2,000-token text | 181 ms | 281 ms | 438 ms |
| One 512 px image | 66 ms | 180 ms | 239 ms |

Text with one question: 550 tokens 91 ms, 8,000 tokens 228 ms, 24,000 tokens 673 ms. A 512 px image
is often enough detail, at a quarter of the tokens of a full-size one.

## Decision harnesses

Endpoints built on Strom for bigger tasks: each splits the task into many decisions and puts the answers together. They use the same API key and balance.

### Document extraction

`POST https://api.uprelic.com/v1/extract` takes one PDF (at most 8 pages) or image and a JSON Schema whose
`properties` are the fields to extract:

```json
{"model": "strom-1.0.7",
 "schema": {"properties": {"invoice_date": {"type": "string", "format": "date"},
                           "total": {"type": "number", "format": "amount", "description": "Total including tax"}}},
 "document": {"url": "https://example.com/invoice.pdf"}}
```

- `document.url`: base64 `data:` URL or public http(s) URL, up to 32 MB. Scans and images are OCR'd.
- Field types: `string` (the whole line), `string` + `format: date` (`YYYY-MM-DD`), `number`, `number` +
  `format: amount` or `percent`, `boolean`, or a string `enum`. `description` says what to look for.
- Response `fields[name]`: `text` (the line Strom picked), `value` (cleaned to the type; the whole line
  when it holds more than one value of the type), `confidence`, `page`, `box` ([left, top, right, bottom]
  as fractions of the page). `null` when the document doesn't give it. Booleans and enums have only
  `value` and `confidence`.
- Charged by input tokens like `/v1/systemone`, once done; needs a positive balance
  to start. Unreadable files, no text or more than 8 pages: 422, free.

### Agent guard

Checks an AI agent's work against rules in plain words at three stages: a tool call before it runs (`before_tool`),
a tool's result before the agent sees it (`after_tool`), and a reply before the user sees it (`before_reply`).

- Rules: `POST https://api.uprelic.com/v1/guard/rules` with `{"id": "refund-limit", "text": "Block refunds above 500
  unless the user's role is manager.", "stage": "before_tool", "tools": ["refund"], "mode": "enforce", "threshold":
  0.9}` creates one (409 if the id is taken). `tools` holds tool names or `"*"` (every tool); leave it out for
  `before_reply`. `mode: observe` only reports. `PUT /v1/guard/rules/{id}` with the settings (no id) changes it as a new version (404
  if it doesn't exist). `GET /v1/guard/rules` lists them, `DELETE /v1/guard/rules/{id}` deletes one. At most 256 per
  account.
- Check: `POST https://api.uprelic.com/v1/guard/check` with `{"action": "refund", "params": {...}, "context":
  {"role": "agent"}}` before the tool runs, the same plus `"result": ...` after it ran, or `{"response": "...",
  "request": "..."}` for a reply. Only rules of that stage are checked. `context` holds facts from your own app,
  never from the model. Checks always use the rules saved on the account; try a new rule in `observe` mode.
- Response: `decision` is `block` (an enforced rule matched: don't run the tool, don't give the agent the result, or
  don't send the reply), `would_block` (only observed rules matched) or `allow`. `rules[]` gives each applying rule's `text`, `probability` of being broken,
  `threshold` and `matched`, most likely first. On a block, return the matched rules' `text` to the agent as the
  tool's result.
- Sessions: `POST https://api.uprelic.com/v1/guard/sessions` opens one per agent task and returns its `id`; send it
  as `session_id` with each check of that task. A check without one is a session of its own.
- Past checks: `GET /v1/guard/checks?decision=would_block&page=1&page_size=25` lists checks still stored (7 days on Free),
  newest first, with what was checked and each rule's result; use it to see what an observed rule would block.
- Python: `@client.guard("refund", context=lambda: {...})` on the tool function raises `uprelic.Blocked`
  instead of running it, also when the check itself fails. `check_result=True` also checks the return value.
- Cost: input tokens at the `strom-fast` price (the action once plus each rule's text), plus $0.50 per 1,000
  sessions past the plan's monthly sessions (Free none, Starter 50,000, Growth 500,000). Free when no rule applies.

### Video detection

Finds the object you describe in plain words in every frame of a video, as a job you poll.

- Start: `POST https://api.uprelic.com/v1/harness/video-detection` with `{"video": {"url":
  "https://example.com/match.mp4"}, "description": "Player with the ball"}`. 202 with the job's `id` and `status:
  "queued"`. The video: a public http(s) URL, MP4, MOV, WebM or another common format, at most 500 MB and 10 minutes.
- Poll: `GET https://api.uprelic.com/v1/harness/video-detection/{id}` every few seconds until `status` is `done` or
  `failed` (`queued`, `running` before; `progress` from 0 to 1). About 30 seconds per video minute.
- Result `frames[]`: one per 0.2 s from the start: `t` (seconds), `box` (`left`, `top`, `right`, `bottom` as
  fractions of the frame, or `null` when the object isn't in it), `confidence` (0–1, `null` with no box). When the
  description fits another object later (the ball is passed), the box moves to it. Kept as long as the plan keeps
  request contents (7 days on Free).
- A failed job has the reason in `error` (video can't be downloaded or read, too long, balance too low) and costs
  nothing.
- Cost: $0.10 per video minute, billed by the second (`usage.video_seconds`, `usage.cost`). Needs a positive balance
  to start (else 402). At most 10 jobs queued or running per account (else 429).

## Designing questions

- **Break broad judgments into narrow ones** and combine them in code. "Should this be escalated?"
  becomes: is a team blocked (`noul`), how urgent (`score`), who owns it (`choice`). The decision rule
  stays visible and easy to change.
- **Ask independent questions over the same state in one request.** The state is read once and
  questions run in parallel, so extra questions are cheap. They can't see each other's answers; use a
  second request only when an answer decides what to ask or fetch next.
- **Give the state enough context** for the judgment: the message, the relevant record fields, the
  policy being checked. Prefer named JSON fields when the state has several parts.
- **Pick the type by what the answer means.** `noul` for whether a condition holds (one per label when
  several can apply); `choice` for exactly one of a set; `score` for degree on an ordered scale. Add
  a `none`/`other` option when nothing may fit.
- **Describe options and levels concretely.** Choice descriptions say when each applies; score levels
  describe distinct situations that stand on their own.
- **Question names are for your code** and aren't sent to the model, so put the full meaning in
  `instructions`.
- **Select instead of generate.** To extract a value, find the candidates in code and let a `choice`
  pick the right one.

## Using the probabilities

- Act automatically above a threshold and send the rest to a person or an LLM. Set thresholds on
  your own data and the cost of a mistake.
- A `noul` near 0.5 means yes and no are similarly likely, not "medium".
- `choice` and `score` `confidence` say how concentrated the probabilities are. Two acceptable options
  can split the probability; that's information, not necessarily an error.
- Strom can pick the wrong option, like any model. Validate on representative cases from the target
  domain before automating.
- Keep API keys server-side; never ship them to a browser or app.

## Docs pages as Markdown

Each page of https://platform.uprelic.com/docs, in full, for when this file isn't enough:

- [Introduction](https://platform.uprelic.com/docs/introduction.md): what Strom does, in two paragraphs.
- [Integrating the API](https://platform.uprelic.com/docs/integration.md): API key, credit, first request with cURL or the Python SDK.
- [Concepts](https://platform.uprelic.com/docs/concepts.md): state, the four question types, models, confidence, repeatability.
- [Pricing and limits](https://platform.uprelic.com/docs/pricing.md): price per token, plans, spend cap, how input tokens are counted, request size, rate limits.
- [Latency](https://platform.uprelic.com/docs/latency.md): measured model time by input tokens, images, questions and options.
- [Data export](https://platform.uprelic.com/docs/data-export.md): downloading everything stored for an account.
- [Zero data retention](https://platform.uprelic.com/docs/zero-data-retention.md): the private endpoint, what is and isn't stored, the subscription.
- [Document extraction](https://platform.uprelic.com/docs/document-extraction.md): `POST /v1/extract`, fields of a JSON Schema from a PDF or image.
- [Agent guard](https://platform.uprelic.com/docs/agent-guard.md): rules, checks of tool calls, tool results and replies, the Python SDK.
- [Video detection](https://platform.uprelic.com/docs/video-detection.md): jobs that find a described object in every frame of a video.

## More

- Hands-on tour with two live calls, no key needed: https://platform.uprelic.com/getting-started-with-strom.md
- Docs: https://platform.uprelic.com/docs
- Site index for agents: https://platform.uprelic.com/llms.txt
- This file as a Claude Code skill:
  `mkdir -p .claude/skills/strom && curl -o .claude/skills/strom/SKILL.md https://platform.uprelic.com/skill.md`
