Text inputs and outputs
Send messages to a model, stream the text it writes, and understand usage and stop reasons.
Text is the core of the gateway. You send messages, and the model streams text back. The examples use the connect helper from the quickstart.
Send text and stream the answer
import { connect } from './gateway.js';
const gateway = await connect();
const result = await gateway.turn(
{
provider: 'auto',
model: 'auto',
messages: [
{ role: 'system', content: [{ type: 'text', text: 'You are a concise assistant.' }] },
{ role: 'user', content: [{ type: 'text', text: 'Explain what a WebSocket is.' }] }
],
params: { max_tokens: 300, temperature: 0.4 }
},
{ onText: (piece) => process.stdout.write(piece) }
);
console.log(result.complete.usage);
gateway.close(); The helper joins the text of every block.delta event whose delta.kind is text. Under the hood, the gateway sends this:
{ "type": "turn.start", "seq": 0, "request_id": "req_8c1f", "provider": "anthropic", "model": "claude-sonnet-5-5", "session_id": "...", "ts": 1791504000000 }
{ "type": "block.start", "seq": 1, "index": 0, "block": { "kind": "text" }, "...": "..." }
{ "type": "block.delta", "seq": 2, "index": 0, "delta": { "kind": "text", "text": "A WebSocket is" },
"usage": { "input_tokens": 24, "output_tokens": 4, "estimated": true },
"budget": { "units_used": 0.42, "units_remaining": 9.58, "limit_window": "month" }, "...": "..." }
{ "type": "block.stop", "seq": 9, "index": 0, "...": "..." }
{ "type": "turn.complete", "seq": 10, "request_id": "req_8c1f", "stop_reason": "stop",
"usage": { "input_tokens": 24, "output_tokens": 61, "estimated": false },
"attempts": [{ "provider": "anthropic", "model": "claude-sonnet-5-5", "outcome": "completed" }], "...": "..." } Every stored event has a session_id, a seq that counts up from zero, a timestamp ts, and the provider and model that produced it.
Messages
A request holds a list of messages. Each message has a role and a content array of parts.
| Role | Used for |
|---|---|
system | Instructions that shape the model’s behavior. Text only |
user | What the person says. Text, images and JSON |
assistant | What the model said earlier. Text and tool calls |
tool | Results of tool calls |
A text part looks like { "type": "text", "text": "..." }. To send structured data the model should read, use a JSON part, { "type": "json", "data": { ... } }. The gateway serializes it for you.
Generation parameters
Put these in params. All are optional.
| Parameter | Range | Effect |
|---|---|---|
max_tokens | 1 or more | Caps the length of the answer |
temperature | 0 to 2 | Higher values are more varied |
top_p | 0 to 1 | Limits sampling to the most likely tokens |
stop | up to 16 strings | Ends the turn when one appears |
When the answer ends because of max_tokens the stop reason is length. When a stop string appears it is stop_sequence.
Choose a model
Set provider and model in the request:
| You set | The gateway does |
|---|---|
| Both to real ids | Uses exactly that model |
Both to auto | Chooses any model that can handle the request |
One provider, model: "auto" | Chooses among that provider’s models |
With auto, the gateway first keeps the models that can do the job: they must support images or tools if you use them, and the conversation plus max_tokens must fit the context window. It then prefers cheaper models. Two optional fields steer the choice:
agent_context.difficultyfrom 1 to 5. Higher values move toward stronger models.agent_context.is_subagent: truemoves one step cheaper, for helper tasks.
To restrict auto to certain models, pass candidates, a list of { "provider": "...", "model": "..." }.
If a named model does not exist you get model_not_found. If nothing can serve the request you get no_capable_model.
Read the live catalog
Models change, so read the list instead of hard-coding it:
curl https://gateway.vaktex.com/vaktex/v1/registry
-H "Authorization: Bearer $VAKTEX_KEY" {
"models": [
{
"provider": "anthropic",
"id": "claude-sonnet-5-5",
"display_name": "Claude Sonnet 5 5",
"type": "text",
"capabilities": { "vision": true, "tools": true, "json_mode": true, "max_context": 1000000, "max_output": 64000 },
"cost": { "input_per_mtok": 2, "output_per_mtok": 10 },
"retention_time": 7776000000,
"retention_reasons": [],
"preview": false
}
]
} Use each model’s provider and id in your requests. Costs are US dollars per million tokens, and retention_time is in milliseconds. Only models that are ready to use are listed, and the response is never cached.
Usage and spend
Each block.delta carries running usage and your budget:
usage.estimatedistrueuntil the provider reports exact counts.turn.completehas the final numbers.budget.units_usedandunits_remainingare US dollars for the tightest limit that applies to you.limit_windowis4h,weekormonth. If you have no limit,units_remainingandlimit_windowarenull.usage.cached_input_tokens, when present, counts input the provider reused from a cache. It is already included ininput_tokens.
You never send usage to the gateway.
Block kinds
Text arrives in blocks. A block opens with block.start, grows with block.delta events and closes with block.stop. Most answers have one text block. Some models also stream a reasoning block before the answer. Show or hide it as you like. A refusal block holds text from a model that declined to answer. The helper in the quickstart only collects text blocks.
Stop reasons
turn.complete.stop_reason says why the turn ended.
| Value | Meaning |
|---|---|
stop | The model finished, or you sent a stop frame |
length | The answer hit max_tokens |
stop_sequence | One of your stop strings appeared |
tool_use | The model is waiting for tool results |
refused | Every candidate model refused |
error | The turn failed. An error event came first |
rate_limited | A spend limit ended the turn |
disconnect | You were away for more than five minutes mid-turn |
interrupted | The session restarted mid-turn. Send the turn again |
Stop a turn early
Send { "type": "stop" } to abort a running turn. You still get a turn.complete with stop_reason: "stop" and the usage so far.
ws.send(JSON.stringify({ type: 'stop' })); Only one turn runs at a time on a session. A second start while one is running returns a session_busy error. You can send the next start as soon as turn.complete arrives.