Skip to content

Build your own helper

A helper is a feature of your product that writes one thing from a description: a summary of a support ticket, a reply to an e-mail, a search query from a question in plain language. You build one on POST /responses with your own instructions and, where your code reads the answer, your own JSON Schema. Name the helper in the helper field and Core meters and traces every call as that helper's work, so you can see what each helper costs. Leave out model and the turn runs on your organisation's helper model.

A helper turn is an ordinary turn otherwise. Everything on Make a model call applies; this page covers what helper adds.

Name the helper

helper is a Core extension on the request: a name you choose, which Core carries on everything the turn spends. Core keeps no list of your helpers and checks a name only against these rules:

  • two to four segments separated by dots, so there is always at least one dot;
  • each segment starts with a lowercase letter, followed by lowercase letters, digits or _;
  • at most 64 characters;
  • not starting with assist., which is reserved for Core's own helpers.

Put your product or company in the first segment, so the name says whose helper it is: acme.summary, acme.ticket_triage, acme.mail.reply. A name that breaks a rule, such as summary, Acme.Summary or assist.summary, is refused with 400 and helper in error.param.

Use one name per helper and keep it stable. The name is what you filter usage by later, so a helper renamed halfway through a month shows up as two.

Use the organisation's helper model

Your organisation can choose a helper model: the model that writes for helpers, the console's own Describe what you need included. An administrator sets it on the organisation's settings, and a key holding management sets it as assist_model with PATCH /organisation (Manage organisations through the API). Without a choice of its own, the organisation uses the deployment's default.

Leave out model on a turn that names a helper, and the turn runs on that helper model. Your code then does not need to know which model the organisation chose, and an administrator can switch models without a change to your product.

Request Model that answers
helper, no model, no agent the organisation's helper model
helper and model the model you named
helper and an agent that fixes a model the agent's model
helper and an agent that fixes no model, no model the organisation's helper model

When no helper model is available, the turn is refused with 409 before any model is called:

Message Meaning What to do
No helper model is configured for this organisation. neither the organisation nor the deployment has chosen a helper model ask an administrator to choose one, or name a model on the turn
The helper model this organisation chose is no longer available. the organisation chose a model that its catalogue no longer holds; Core does not fall back to the deployment's default ask an administrator to choose another model

A key holding the assist capability can check in advance with GET /assist, which answers {"model": ..., "source": ...}: the helper model in force and whether the organisation or the deployment chose it, or null for both when there is none. A helper turn itself needs no capability beyond what any turn needs.

Send a helper turn

This helper writes a short summary of a support ticket and gives it a priority. The answer follows a JSON Schema, so your code reads fields instead of parsing text (Structured output). store: false keeps nothing of the turn: a summary like this is shown once and never continued.

curl -s "$CORE_URL/responses" \
  -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
  -d '{
    "helper": "acme.summary",
    "instructions": "Summarise the support ticket in at most two sentences and rate its priority.",
    "input": "Since this morning no customer can log in to the portal. We get error 502.",
    "store": false,
    "text": {
      "format": {
        "type": "json_schema",
        "name": "ticket_summary",
        "strict": true,
        "schema": {
          "type": "object",
          "properties": {
            "summary": { "type": "string" },
            "priority": { "type": "string", "enum": ["low", "normal", "high"] }
          },
          "required": ["summary", "priority"],
          "additionalProperties": false
        }
      }
    }
  }'

The answer is an ordinary response object. output_text holds the JSON your schema describes, and model names the helper model that answered. The response does not repeat helper: neither this answer nor GET /responses/{id} carries it. When the model's answer does not match the schema, the turn fails with invalid_structured_output and you get no partial answer.

The OpenAI Python SDK does not know helper, so pass it through extra_body:

import json
from openai import OpenAI

client = OpenAI(base_url="https://<your-deployment>/api/v1", api_key="ack_...")

response = client.responses.create(
    instructions="Summarise the support ticket in at most two sentences and rate its priority.",
    input="Since this morning no customer can log in to the portal. We get error 502.",
    store=False,
    text={
        "format": {
            "type": "json_schema",
            "name": "ticket_summary",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                    "priority": {"type": "string", "enum": ["low", "normal", "high"]},
                },
                "required": ["summary", "priority"],
                "additionalProperties": False,
            },
        }
    },
    extra_body={"helper": "acme.summary"},
)
ticket = json.loads(response.output_text)

helper exists only on POST /responses. POST /messages has no such field.

Keep the turn, or not

store works as on any turn (Storage settings). For a one-off helper, such as a summary, a translation or a draft your user edits further in your own product, send store: false: the call answers 200 and Core keeps no conversation. Leave store as it is when you want to read the turn back or continue it, for instance a helper your user can ask to try again. Usage is metered in both cases.

Rules that apply as on any turn

Naming a helper does not loosen anything. The turn is your key's own turn, under your own instructions:

  • Policy. Your key's policy decides whether the helper model, or the model you named, may be used and where it may run. A model the policy refuses is answered 403, as on any turn.
  • Caps. Your key's and your organisation's monthly caps admit the turn. A reached cap answers 429.
  • Personal data. When your key holds pseudonymisation, personal data in the description is hidden from the model as on every turn (Pseudonymisation).
  • Tools, fallback and streaming work as on any turn. A helper turn may call tools, falls back along the model's fallback chain and can stream.

Helpers beyond one call

The helper name stays with the work wherever the turn takes it:

  • A background run ("background": true, Run a turn in the background) keeps the helper for every round.
  • A task an agent starts from a helper turn is the helper's work too: the task's run and what it spends carry the same helper name.
  • An agent called as a tool during a helper turn spends for the helper as well. Its usage rows name both the agent and the helper.
  • A schedule stores helper with the request it fires, but that request must name a model, or an agent version that fixes one. Core checks a schedule's request when you write it, and the helper model is only looked up when a turn starts, so a request with a helper and no model is refused with 422 on request.model: Name a model, or an agent version that fixes one. (Schedules).

See what a helper costs

Every usage row the turn writes carries the helper's name in helper: the model calls, the tools it ran and the turns of agents it called. Core's own helpers show up beside yours under their own names, such as assist.json_schema for the JSON Schema draft in the console.

  • GET /usage?helper=acme.summary lists one helper's rows. The match is exact, never a prefix: acme does not find acme.summary.
  • GET /usage/summary splits every group by helper, so each total carries helper beside model and agent. Rows no helper wrote form groups with helper: null.
  • On the console's Usage section, the Cost tab has a Per helper card: one line per helper with what its work cost in the chosen period (Usage in the console).
  • On the console's Traces section, enter the helper's name under More filters in Job kind to see only that helper's model calls (Read a trace). This filter is in the console only; no trace list a key reads takes it.
curl -s "$CORE_URL/usage?helper=acme.summary&from=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer $CORE_KEY"

More on the usage routes is in Usage and audit through the API.