Streaming
Set stream to true and Kalle sends the answer while it writes it, as server-sent events. People see the first words almost at once instead of waiting for the whole answer.
curl -N https://api.kalle.build/v1/messages \
-H "Authorization: Bearer $KALLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kalle-turbo",
"max_tokens": 1024,
"stream": true,
"messages": [
{
"role": "user",
"content": "Write a short welcome email for new members of a climbing gym."
}
]
}'const response = await fetch("https://api.kalle.build/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KALLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "kalle-turbo",
max_tokens: 1024,
stream: true,
messages: [
{
role: "user",
content: "Write a short welcome email for new members of a climbing gym."
}
]
}),
});
const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const event = JSON.parse(line.slice(6));
if (event.type === "text.delta") process.stdout.write(event.text);
}
}import json
import os
import requests
with requests.post(
"https://api.kalle.build/v1/messages",
headers={"Authorization": f"Bearer {os.environ['KALLE_API_KEY']}"},
json={
"model": "kalle-turbo",
"max_tokens": 1024,
"stream": True,
"messages": [
{
"role": "user",
"content": "Write a short welcome email for new members of a climbing gym."
}
]
},
stream=True,
) as response:
response.raise_for_status()
for line in response.iter_lines(decode_unicode=True):
if line.startswith("data: "):
event = json.loads(line[6:])
if event["type"] == "text.delta":
print(event["text"], end="", flush=True)package main
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"model": "kalle-turbo",
"max_tokens": 1024,
"stream": true,
"messages": [
{
"role": "user",
"content": "Write a short welcome email for new members of a climbing gym."
}
]
}`)
req, _ := http.NewRequest("POST", "https://api.kalle.build/v1/messages", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KALLE_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
scanner := bufio.NewScanner(res.Body)
for scanner.Scan() {
data, ok := strings.CutPrefix(scanner.Text(), "data: ")
if !ok {
continue
}
var event struct {
Type string `json:"type"`
Text string `json:"text"`
}
if json.Unmarshal([]byte(data), &event) == nil && event.Type == "text.delta" {
fmt.Print(event.Text)
}
}
}require "json"
require "net/http"
uri = URI("https://api.kalle.build/v1/messages")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("KALLE_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = <<~JSON
{
"model": "kalle-turbo",
"max_tokens": 1024,
"stream": true,
"messages": [
{
"role": "user",
"content": "Write a short welcome email for new members of a climbing gym."
}
]
}
JSON
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request) do |response|
buffer = +""
response.read_body do |chunk|
buffer << chunk
while (line = buffer.slice!(/.*\n/))
next unless line.start_with?("data: ")
event = JSON.parse(line.delete_prefix("data: "))
print event["text"] if event["type"] == "text.delta"
end
end
end
end<?php
$body = <<<'JSON'
{
"model": "kalle-turbo",
"max_tokens": 1024,
"stream": true,
"messages": [
{
"role": "user",
"content": "Write a short welcome email for new members of a climbing gym."
}
]
}
JSON;
$buffer = "";
$ch = curl_init("https://api.kalle.build/v1/messages");
curl_setopt_array($ch, [
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("KALLE_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$buffer) {
$buffer .= $chunk;
while (($end = strpos($buffer, "\n")) !== false) {
$line = substr($buffer, 0, $end);
$buffer = substr($buffer, $end + 1);
if (str_starts_with($line, "data: ")) {
$event = json_decode(substr($line, 6), true);
if ($event["type"] === "text.delta") {
echo $event["text"];
}
}
}
return strlen($chunk);
},
]);
curl_exec($ch);import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class KalleStream {
public static void main(String[] args) throws Exception {
String body = """
{
"model": "kalle-turbo",
"max_tokens": 1024,
"stream": true,
"messages": [
{
"role": "user",
"content": "Write a short welcome email for new members of a climbing gym."
}
]
}
""";
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.kalle.build/v1/messages"))
.header("Authorization", "Bearer " + System.getenv("KALLE_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(line -> line.startsWith("data: "))
.map(line -> line.substring(6))
.forEach(System.out::println);
}
}What the stream looks like
Every event has an event: line and a data: line with JSON. The JSON always has a type, so you can ignore the event: lines if your client makes that easier.
event: message.start
data: {"type":"message.start","message":{"id":"msg_01J8Z6","model":"kalle-turbo"}}
event: text.delta
data: {"type":"text.delta","index":0,"text":"Welcome to the wall"}
event: text.delta
data: {"type":"text.delta","index":0,"text":", Maja! Your first session"}
event: message.done
data: {"type":"message.done","stop_reason":"end_turn","usage":{"input_tokens":24,"output_tokens":186}}Event types
| Event | When it arrives | Data |
|---|---|---|
message.start | Once, first. | message.id and message.model. |
text.delta | Many times. | index of the content block and the next piece of text. |
tool_use | When Kalle calls a tool. | The complete call: id, name and input. |
message.done | Once, last. | stop_reason and the final usage. |
ping | Now and then. | Nothing. Keeps the connection open through proxies. |
error | If something fails mid-stream. | An error object, as described in Errors. |
Join the text of every text.delta with the same index to get the full text block. tool_use arrives whole, so you never have to stitch together half a JSON object.
Errors during a stream
The HTTP status is sent before the first event, so a bad request or an invalid key still fails with a normal error response. Once the stream has started, problems arrive as an error event and the stream ends.
Streaming through your own server
Your API key must stay on your server, so stream from Kalle to your server and on to the browser. Most frameworks can return the response body as a stream. Forward the events as they arrive and do not buffer the whole answer first.