Completion¶
POST /v1/completion is the engine's front door. You give it an input and a mode; it enqueues a run and returns a job envelope. The run itself is asynchronous — read the result from /v1/jobs/{id}.
Want to know the price first? Send the same body to POST /v1/completion/estimate — it answers what the run would cost without running anything or charging anything.
POST /v1/completion¶
Enqueue an engine run. Returns a pending JobStatus with an id of the form job-<uuid>.
Input: prompt or messages¶
Provide exactly one of these (supplying both or neither is a 422):
| Field | Type | Description |
|---|---|---|
prompt | string | A single instruction or question. Up to 500,000 characters. |
messages | array | An OpenAI-format chat history — a list of { "role", "content" } turns. The last turn must be role: "user". |
For messages, role is one of system, user, or assistant; each content is capped at 500,000 characters and the summed length of all content at 8,000,000 characters (1–256 turns). The history budget is deliberately far larger than the single-field one, so a long conversation that carries file reads or pasted documents can be replayed in full; what actually bounds such a run is the model's context window and the pre-flight cost check — a history far past the chosen model's window is refused with a model budget 422 rather than messages_too_large, so pick a wide-window model in settings.model when you replay a long conversation.
Body¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
prompt | string | one of | — | Single-string input (see above). |
messages | array | one of | — | Chat-history input (see above). |
mode | string | ✓ | — | Which orchestration to run. See Modes. |
main | string | by mode | — | The answering or lead companion (name or cmp_<uuid>). Required for answer and parallel_with_main; not used by parallel. Never inferred. |
participants | string[] | by mode | — | The companions to involve: names, cmp_<uuid> ids or team_<uuid> ids. Required for every multi-persona mode; not allowed for answer. Never inferred. |
stream | boolean | — | false | Hint that you intend to subscribe to the event stream. |
settings | object | — | — | Per-call overrides. See Settings. |
tools | array | — | — | Client-side tools the model may call. See Client tools. |
Companions and teams
A companion is a persona; a team groups companions. List the ones you can use with GET /v1/discover, and pass their names or ids as main / participants.
Choosing companions¶
Every request names the companions it runs. Nothing is filled in from a saved preference, an earlier run or a persona: a request that leaves a required field out is refused with 422 (main_required, participants_required), and an answer request that carries participants is refused with participants_not_allowed — put the one companion in main instead.
| Mode | main | participants |
|---|---|---|
answer | required | not allowed |
parallel | not used | required |
parallel_with_main | required — the synthesizer | required |
A participants entry may be a team_<uuid> id: the team expands to its members visible to you, in a stable order, and the run stores that roster, so a later change to the team does not touch a submitted run. A team you cannot see, an empty team, a companion listed twice (directly or through two teams), or an expanded roster above 15 companions is refused, never trimmed. A bare name is always a companion, never a team.
Saved preferences still supply default settings underneath your per-call values; they no longer choose companions. If you relied on an omitted main or participants, send the companions you want on every call.
Modes¶
| Mode | What it produces |
|---|---|
answer | One companion's answer. Set settings.crumbs: true to deepen it over internal reasoning iterations. |
parallel | Every participant answers independently — N responses, no synthesis. |
parallel_with_main | Participants answer in parallel; a lead companion synthesizes them. |
GET /v1/params is the live list of modes available to your key; a mode that is not listed there is refused with 422.
Settings¶
settings is the single per-call override object. Every field is optional.
| Field | Type | Description |
|---|---|---|
model | string | Model slug for the answering personas. Must appear in GET /v1/models. |
temperature | number | Sampling temperature, range-checked against the model's bounds. |
top_p | number | Nucleus-sampling cutoff, range-checked against the model's bounds. |
crumbs | boolean | Toggle the reasoning-deepening loop. On mode: "answer", crumbs: true deepens the single answer over internal iterations (the run stays mode: "answer"). |
zdr | boolean | Per-call Zero Data Retention. When true, the run uses only non-retaining providers (some models become unavailable) and only vendors vetted for it — web search is dropped while it is on. It does not change how long the run's content is kept. Omit to inherit your account default. |
web_search | boolean | Per-call simple web search — quick lookups, run provider-side. When true, personas can search the web and cite live sources. Omit to inherit your account default. |
budget_guard | boolean | Per-call budget guard. When false, this run skips the pre-flight cost check even if your account default is on. Omit to inherit your account default. |
Examples¶
import os, requests
r = requests.post(
"https://api.humx.ai/v1/completion",
headers={"Authorization": f"ApiKey {os.environ['COMPANIONS_API_KEY']}"},
json={
"prompt": "What are the trade-offs of optimistic locking?",
"mode": "answer",
"main": "Ada",
"settings": {"temperature": 0.4},
},
)
r.raise_for_status()
job = r.json()
print(job["id"], job["status"]) # job-... pending
const res = await fetch("https://api.humx.ai/v1/completion", {
method: "POST",
headers: {
Authorization: `ApiKey ${process.env.COMPANIONS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "What are the trade-offs of optimistic locking?",
mode: "answer",
main: "Ada",
settings: { temperature: 0.4 },
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const job = await res.json();
console.log(job.id, job.status); // job-... pending
interface JobStatus {
id: string;
kind: "completion" | "companion" | "team" | "forensics";
status: "pending" | "running" | "complete" | "failed" | "requires_action";
content?: unknown;
}
const res = await fetch("https://api.humx.ai/v1/completion", {
method: "POST",
headers: {
Authorization: `ApiKey ${process.env.COMPANIONS_API_KEY!}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "What are the trade-offs of optimistic locking?",
mode: "answer",
main: "Ada",
settings: { temperature: 0.4 },
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const job = (await res.json()) as JobStatus;
A multi-persona example — a parallel run across three companions (parallel takes no main):
{
"prompt": "Should we adopt event sourcing for the orders service?",
"mode": "parallel",
"participants": ["architect", "sre", "product-lead"]
}
The same question put to a whole team, with a lead who synthesizes the answers:
{
"prompt": "Should we adopt event sourcing for the orders service?",
"mode": "parallel_with_main",
"main": "architect",
"participants": ["team_3f9c1e2a-7b4d-4c8e-9a1f-2d5e6c7b8a90"]
}
Response¶
200 — a pending job envelope. The answer arrives later via /v1/jobs/{id}.
{
"type": "status",
"id": "job-1a2b3c4d-...",
"kind": "completion",
"status": "pending",
"mode": "answer"
}
Once complete, the same job carries a typed content keyed by shape:
{
"id": "job-1a2b3c4d-...",
"kind": "completion",
"status": "complete",
"content": {
"shape": "answer",
"companion": "Ada",
"response": "Optimistic locking trades..."
}
}
See Jobs → Result shapes for every mode's content.
Estimating a run¶
POST /v1/completion/estimate takes the exact same body as POST /v1/completion and answers what that run would cost instead of running it. Nothing is created and nothing is charged — no job, no balance movement. To budget a call, re-send what you were about to send:
curl https://api.humx.ai/v1/completion/estimate \
-H "Authorization: ApiKey $COMPANIONS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Should we rewrite the billing service in Rust?",
"mode": "parallel_with_main",
"main": "architect",
"participants": ["architect", "sre"]
}'
{
"mode": "parallel_with_main",
"currency": "USD",
"estimable": true,
"runs": 1,
"estimate": { "expected": 0.0431, "low": 0.0189, "high": 0.1204 },
"calls": { "expected": 14, "high": 14 },
"confidence": "medium",
"basis": "mixed",
"caveats": [],
"reasons": [],
"balance": { "current": 2.5, "sufficient": true },
"stages": [ { "...": "one row per stage, for auditing the total" } ]
}
Read it in this order: estimable → confidence and basis → estimate.expected and balance.sufficient → caveats and stages.
The band. expected prices the calls the run must make at each stage's typical output length; low is the same calls at the 10th percentile; high is every call — including optional ones such as a tool loop running to its cap — at the 90th percentile. Budget against expected; size headroom against high. Money has four decimal places, matching your billing ledger.
confidence (high / medium / low) is the worst grade of any input the estimate consumed. One stage with no usable history drags the whole record down — deliberately, so you never have to read stages[] to learn the number is soft.
basis says where the numbers came from:
| Value | Meaning |
|---|---|
personal | your own past runs of this stage |
companion | a public persona's cross-user history |
global | the cross-user rollup for this stage |
catalogue | at least one call has no run history and was priced from the model price catalogue |
constant | no history anywhere; built-in fallback token counts |
mixed | more than one of the above contributed |
estimable: false is an answer, not an error. A run whose cost cannot be known ahead of time comes back 200 with every number zeroed, balance.sufficient: null, and stable machine-readable tokens in reasons[] — for example run_structure_not_predictable:<mode> (the graph's shape is decided at run time), no_rate_for_model:<slug> (a model with no history and no catalogue price — unknown, not free), or resolution_failed:<code> (the same problem the real POST would refuse with a 4xx, e.g. an ambiguous companion name). Branch on reasons; caveats is the human-readable half and is free to change.
Relationship to the budget guard
The budget guard runs this same pricing before every enqueue and refuses with 402 when expected exceeds your balance. The estimate endpoint itself is never guarded — asking the price is always free — and a non-estimable run always passes the guard: an unknown is not a refusal.
Client tools¶
Attach a tools array to declare functions the model can call on your machine (available for mode: "answer", with or without settings.crumbs). When the model calls one, the run pauses and hands the call back — the server never executes your tools. You run them and return the results via POST /v1/jobs/{id}/tool_outputs.
{
"prompt": "What's in my workspace?",
"mode": "answer",
"tools": [
{
"name": "read_user_file",
"description": "Read a file from the user's local workspace.",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "Path relative to workspace root" }
},
"required": ["path"]
}
}
]
}
parameters is plain, provider-neutral JSON Schema. Bounds: up to 32 tools; each name matches ^[a-zA-Z_][a-zA-Z0-9_]*$ (1–256 chars, unique per request); parameters ≤ 16 KiB serialized. Names longer than a model provider accepts are carried over the provider wire under a reversible substitute and handed back to you unchanged, so the name you declare is the only name you handle. The full pause/resume loop lives on the Jobs page.
Validation¶
A 422 is returned before any run is enqueued when, for example:
type | Trigger |
|---|---|
prompt_or_messages | Both or neither of prompt / messages supplied. |
last_message_not_user | The final messages turn is not role: "user". |
messages_too_large | Summed content exceeds 8,000,000 characters. |
unknown_model | settings.model is not in your GET /v1/models allow-list. |
value_error (temperature / top_p) | The value is outside the model's published range. |
Insufficient balance surfaces as 402 — before anything is enqueued when the budget guard prices the run above your balance (the body then carries estimate, balance, and shortfall), or when a running call is charged.