Skip to content
Text3 min

Stream chat replies

Call text models with the chat completions format, stream the reply token by token and read the exact charge from the last chunk.

You need

  • A Tovano API key in TOVANO_API_KEY
  • Node 18+ or any HTTP client that reads server-sent events

Text models use the chat completions format that most chat clients already speak. If you have code that calls a chat model today, you change the base URL to https://api.tovano.ai/v1, the key and the model name.

text/demo-chat-1 costs $1.00 per million input tokens and $4.00 per million output tokens.

1. A plain request

curl https://api.tovano.ai/v1/chat/completions \
  -H "Authorization: Bearer $TOVANO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text/demo-chat-1",
    "messages": [
      { "role": "system", "content": "You write short, factual product descriptions." },
      { "role": "user", "content": "Describe an insulated steel water bottle in two sentences." }
    ],
    "max_tokens": 200
  }'

The reply has the message and the token counts in usage.

2. Stream the reply

Add "stream": true. The reply arrives as server-sent events, one data: line per piece, and ends with data: [DONE]. To get the token counts and the charge, also ask for usage:

curl -N https://api.tovano.ai/v1/chat/completions \
  -H "Authorization: Bearer $TOVANO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text/demo-chat-1",
    "messages": [{ "role": "user", "content": "Three captions for a mug ad" }],
    "stream": true,
    "stream_options": { "include_usage": true }
  }'

The last chunk before [DONE] carries the usage and a tovano block with the task ID and the charge:

{
  "choices": [],
  "usage": { "prompt_tokens": 42, "completion_tokens": 61, "total_tokens": 103 },
  "tovano": { "task_id": "tsk_c81d20a4f6e1", "charged_micro": 286 }
}

42 input tokens at $1.00 per million and 61 output tokens at $4.00 per million make $0.000286.

3. Stream with the SDK

The SDK asks for usage automatically and gives you an async iterator:

import { Tovano } from "tovano";

const tovano = new Tovano();

const stream = tovano.chat.completions.create({
  model: "text/demo-chat-1",
  messages: [{ role: "user", content: "Three captions for a mug ad" }],
  max_tokens: 300,
  stream: true,
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? "");
  if (chunk.usage) console.log("\n", chunk.usage);
}

4. Pass the stream to a browser

Never put your API key in browser code. Stream from your server and pass the text on:

// A route on your server, for example in Bun or any fetch-based framework.
export async function POST(req: Request) {
  const { prompt } = await req.json();
  const upstream = await fetch("https://api.tovano.ai/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TOVANO_API_KEY}`,
      "Content-Type": "application/json",
      "X-Tovano-Customer": "client-0482",
    },
    body: JSON.stringify({
      model: "text/demo-chat-1",
      messages: [{ role: "user", content: prompt }],
      stream: true,
    }),
  });
  return new Response(upstream.body, { headers: { "Content-Type": "text/event-stream" } });
}

How chat is charged

  • While the request runs, the input tokens plus max_tokens are reserved. Set max_tokens to what you really need so less of your balance is held.
  • When the reply ends, you are charged for the tokens actually used and the rest is released.
  • Every chat call is recorded as a task, so it shows up in Tasks and Usage with its customer tag.
  • If the call fails before the model answers, nothing is charged.

Stuck? Write to support@tovano.ai with the task ID and the X-Request-Id of the response.