Endpoint
POST https://evaesi.dev/api/v1/responsesThe older POST /api/v1/chat route remains mounted as a compatibility alias, but /api/v1/responses is the forward contract.
The request
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes | Prompt text to dispatch through the inference gateway. |
model | string? | No | Model identifier. Canonical values are "instant", "reasoning", and "elements"; common aliases normalise case-insensitively (see Models). If omitted, selection is delegated to the inference gateway, the response's model field reports what was actually used. |
max_output_rungs | u64? | No | Output ceiling forwarded to the gateway, validated against the selected model's output limit, a value of 0 or one above the limit returns 400 INVALID_REQUEST. The accepted value is used verbatim for budget reservation and daily-throughput charging. Defaults and limits per model: instant 32,000; reasoning and elements 64,000. |
include_regions | bool? | No | Expose region metadata in usage where available. |
studio_id | string? | No | Accepted but not yet wired, the field is parsed and currently ignored, and the request dispatches with no studio context. Do not depend on it. |
custom_instructions | string? | No | Optional instruction override. |
web_search | bool | No | Favor fresh retrieval where supported. |
code_mode | bool | No | Code and documentation mode. |
image_mode | bool | No | Image or visual mode. |
stream | bool? | No | Enable SSE streaming on the same route. |
Live validation rejects empty prompts and prompts over 100000 bytes. If include_regions is enabled today, the field is reserved in the response but currently reports 0 because the live gateway does not emit region activations yet.
Response
{
"request_id": "uuid",
"content": "Generated text...",
"sources": [],
"sources_url": "/api/v1/sources/uuid",
"usage": {
"input_rungs": 29,
"output_rungs": 118,
"total_rungs": 147,
"regions_activated": null
},
"model": "instant"
}The live response payload emits sources_url as /api/v1/sources/{request_id}. The nested /api/v1/responses/{request_id}/sources path remains mounted as a compatible alias.
Two further optional fields are omitted from the JSON entirely (not null) when absent: evidence_verdict (string, the EsiGo evidence verdict when the pipeline produced one) and route (object, route metadata from the inference edge: route_id, selected_entry_point, primary_model, evidence_model, route_class, studio_required, approval_required, decided_at_ns).
Streaming
When stream: true, the same endpoint returns server-sent events. Current live event names are:
| Event | Meaning |
|---|---|
response.output_text.delta | Partial output chunk |
response.completed | Final completion payload |
response.error | Stream-level error payload |
At stream start, the response headers include input-rung and rate-limit metadata (x-evaesi-input-rungs, x-ratelimit-limit, x-ratelimit-remaining). Output and total rung headers are not sent on the streaming path, instead the final response.completed event carries a usage object (input_rungs, output_rungs, total_rungs). Those values are committed to billing before the final event is sent, so they always match what GET /api/v1/usage shows afterwards. Streamed responses carry no sources_url: construct it yourself as /api/v1/sources/{request_id} from any chunk's request_id.
Each data: line is a JSON-serialized InferenceChunk. Only index, content, and done are guaranteed on every chunk; request_id, model, sources, usage, and the other optional fields are omitted when absent or empty, "sources":[] never appears on the wire, and usage appears only on the final event.
event: response.output_text.delta
data: {"index":0,"content":"The ","done":false,"request_id":"req_abc","model":"instant"}
event: response.output_text.delta
data: {"index":1,"content":"answer ","done":false,"request_id":"req_abc","model":"instant"}
event: response.completed
data: {"index":42,"content":"","done":true,"request_id":"req_abc","model":"instant","usage":{"input_rungs":29,"output_rungs":118,"total_rungs":147}}Streamed requests always return HTTP 200. The response head is sent before inference is attempted. Errors arrive in-band as a response.error event whose data is {"error":{"code","message"}}. A mid-stream failure emits response.error followed by a final response.completed; a failure to start the stream emits response.error with no completion event. Handle response.error explicitly, never rely on the HTTP status to detect streaming failures. The stream also sends SSE keep-alive comment lines (prefixed :) during idle periods; ignore any line not prefixed data: .
Examples
cURL
curl -X POST https://evaesi.dev/api/v1/responses \
-H "Authorization: Bearer esi_dev_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "What is the capital of France?",
"model": "instant",
"web_search": true
}'Python
import requests
response = requests.post(
"https://evaesi.dev/api/v1/responses",
headers={"Authorization": "Bearer esi_dev_YOUR_KEY"},
json={
"prompt": "What is the capital of France?",
"model": "instant",
"web_search": True,
},
)
data = response.json()
print(data["content"])
print(f"Used {data['usage']['input_rungs']} input rungs")JavaScript
const response = await fetch("https://evaesi.dev/api/v1/responses", {
method: "POST",
headers: {
"Authorization": "Bearer esi_dev_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "What is the capital of France?",
model: "instant",
web_search: true,
}),
});
const data = await response.json();
console.log(data.content);Streaming Example
const response = await fetch("https://evaesi.dev/api/v1/responses", {
method: "POST",
headers: {
"Authorization": "Bearer esi_dev_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "Explain gravity.",
model: "instant",
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
for (const line of text.split("\n")) {
if (line.startsWith("data: ")) {
const chunk = JSON.parse(line.slice(6));
if (chunk.error) {
throw new Error(`${chunk.error.code}: ${chunk.error.message}`);
}
if (chunk.done) {
console.log("\nrequest_id:", chunk.request_id);
break;
}
process.stdout.write(chunk.content ?? "");
}
}
}