OP Inference

Tasks

Call a ready-made task, such as sentiment or receipt extraction, and get JSON that matches its schema.

A task is a named job with a fixed input and a fixed output: you send the input, and the answer is JSON that has been checked against the task's output schema. We write and version each task's prompt, models and schema, so you do not write a prompt or a response_format yourself.

curl https://api.opinference.com/v1/tasks/sentiment \
  -H "Authorization: Bearer $OP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "The battery lasts all day, but the screen scratches easily."}'
{
  "id": "gen_...",
  "object": "task.result",
  "task": "sentiment@1",
  "model": "openai/gpt-oss-120b",
  "output": {"label": "mixed", "score": 0.1},
  "usage": {"input_tokens": 212, "output_tokens": 18},
  "op": {"generations": ["gen_..."]}
}

Routes

RouteWhat it does
GET /v1/tasksLists every task your key may call, with its input and output schema and an estimated price
POST /v1/tasks/{name}Runs the task's latest version. The body is the input object itself
POST /v1/tasks/{name}@{n}Runs version n. A version never changes once published, so pin one when you depend on its output

A key may call a task only if it may use every model the task lists. GET /v1/tasks shows only those tasks, and POST refuses the others with 403 forbidden.

How an answer is checked

A task request is served as a non-streaming chat completion with the task's prompt and its output schema as a strict response_format. The gateway then checks the answer:

  1. A refusal from the model ends the request with 502 output_invalid and reason: "refusal".
  2. An answer cut off at the task's output limit ends it with reason: "length".
  3. An answer that is not JSON, or does not match the output schema, is sent back to the model once with the validator's message. If the second answer also fails, the request ends with reason: "schema" and param names the failing JSON pointer.

output is always the checked answer, never the provider's raw reply.

Billing

Each generation is billed as a chat completion of the model that answered. A repaired request is two generations and bills twice; op.generations lists both, in order, and usage is their sum. Your budget must cover two generations before the request is admitted, and what the repair did not use is released.

Idempotency-Key covers every generation of a request. A retry with the same key after any generation was billed returns 409 idempotency_conflict rather than running again, so send a new key to retry an output_invalid.

Accuracy targets

For the extraction tasks, we score answers against a committed corpus of at least 50 cases each, counting every field: precision, recall and their F1. The figures below are targets, not measurements. A measured figure is published here only from a capped run against live providers, with its date, model, corpus version and case count.

TaskTarget field F1Measured
receipt-extract≥ 0.90 (target)Not yet measured
entities≥ 0.90 (target)Not yet measured
keywords≥ 0.80 (target)Not yet measured
resume-extract≥ 0.90 (target)Not yet measured

resume-extract is scored on synthetic résumés: every person, employer and school in its corpus is invented, so no real person's data is involved. The task is served once its models pass the file check.

The tasks

Each example shows a request body and an answer of the shape you get back. The answers are illustrations, not recorded output.

sentiment

{"text": "The battery lasts all day, but the screen scratches easily."}
{"label": "mixed", "score": 0.1}

label is positive, neutral, negative or mixed; score runs from -1 to 1.

classify

{"text": "My card was charged twice for one order.", "labels": ["billing", "shipping", "account"]}
{"labels": [{"label": "billing", "score": 0.97}]}

Set multi_label to true to allow more than one label.

entities

{"text": "Ada Lovelace met Charles Babbage in London in June 1833.", "types": ["person", "location", "date"]}
{
  "entities": [
    {"text": "Ada Lovelace", "type": "person"},
    {"text": "Charles Babbage", "type": "person"},
    {"text": "London", "type": "location"},
    {"text": "June 1833", "type": "date"}
  ]
}

Types are person, organization, location, date, money, product, event and other. Without types, every type is returned.

keywords

{"text": "Solar panels convert sunlight into electricity using photovoltaic cells...", "max_keywords": 3}
{"keywords": ["solar panels", "photovoltaic cells", "electricity"]}

max_keywords is 1 to 20, and 10 when absent.

summarize

{"text": "...", "max_words": 60, "style": "bullets"}
{"summary": "- The council approved the new budget.\n- Spending on parks rises by 4%."}

style is paragraph (the default) or bullets. max_words is 10 to 500, and 120 when absent.

translate

{"text": "Where is the train station?", "target_language": "de"}
{"translation": "Wo ist der Bahnhof?", "source_language": "en"}

target_language is a language name or an ISO 639-1 code. source_language is detected when absent.

grammar

{"text": "Their going to the park tomorow."}
{
  "corrected": "They're going to the park tomorrow.",
  "edits": [
    {"original": "Their", "corrected": "They're"},
    {"original": "tomorow", "corrected": "tomorrow"}
  ]
}

prompt-optimize

{"prompt": "write about dogs", "goal": "A short blog intro for new dog owners"}
{
  "prompt": "Write a friendly 120-word introduction for a blog aimed at first-time dog owners...",
  "changes": ["Named the audience", "Set a length", "Set the tone"]
}

workflow-draft

{
  "description": "Caption an image, then write a one-line alt text from the caption.",
  "models": [{"id": "openai/gpt-5-mini", "modality": "text"}]
}
{"definition": "{\"schema\": \"op.workflow/1\", \"slug\": \"caption-alt-text\", ...}"}

definition is a workflow in the op.workflow/1 format, as JSON text, for you to review and save. The task checks only that it is text: it is not checked against the workflow rules or the catalog, so it can name a model that does not exist or break a rule a save enforces. Saving it is what checks it. description is 1 to 4,000 characters. models lists at most 500 model ids the draft may use, each with its modality; without it, the draft names models from what the model knows, which may not be in our catalog.

receipt-extract

image is an https URL or a data: URI of an image. The task runs only on models that accept images.

{"image": "https://example.com/receipt.png"}
{
  "document_type": "receipt",
  "merchant_name": "Quayside Fish Bar",
  "merchant_address": "8 Mill Lane, Lakeview",
  "merchant_tax_id": null,
  "invoice_number": "R-60040",
  "date": "2026-10-24",
  "time": "08:35",
  "currency": "EUR",
  "line_items": [
    {"description": "Garden salad", "quantity": 4, "unit_price": 9.59, "total": 38.36}
  ],
  "subtotal": 38.36,
  "tax": 3.07,
  "tip": null,
  "total": 41.43
}

A field the document does not show is null. Dates are YYYY-MM-DD and times 24-hour.

Tasks that read a PDF

A task that reads a PDF takes it in one input field as data:application/pdf;base64,..., under the rules for a PDF part. The PDF is sent to the model as a file, not as text. A field that holds anything else, such as an image or a URL, is refused with 400 invalid_request, and param names the field. Such a task runs only on endpoints that have passed our PDF check. resume-extract will be the first of these tasks.

Errors

StatusCodeWhen
400invalid_requestThe input does not match the task's input schema. param names the first failing field
413payload_too_largeA PDF input is over 10 MiB. param names the field
403forbiddenYour key may not use a model the task lists
404model_not_foundNo such task, or no such version
404no_endpoint_availableNo endpoint of the task's models can serve it now
409idempotency_conflictThe key was used and a generation was already billed
502output_invalidThe answer could not be used. reason is schema, refusal or length

See Errors for the envelope.