anoman
Docs · Streaming

Server-Sent Events untuk output token demi token.

Stream output model saat dihasilkan. Time-to-first-token di bawah satu detik, metadata guardrail lengkap pada event terakhir, dan penanganan error di tengah stream yang mulus.

Mulai Cepat

Aktifkan streaming

Setel stream: true pada body request. Response memakai Server-Sent Events dengan Content-Type: text/event-stream. SDK OpenAI dan Anthropic menangani parsing SSE secara otomatis.

from openai import OpenAI

client = OpenAI(base_url="https://api.anoman.io/v1", api_key="anm-sk-...")

stream = client.chat.completions.create(
    model="claude-sonnet-4-6",
    messages=[{"role": "user", "content": "Tell me a short story"}],
    stream=True,
)

for chunk in stream:
    # OpenAI SDK parses SSE for you — chunk is a typed object
    delta = chunk.choices[0].delta.content or ""
    print(delta, end="", flush=True)
Catatan proxy edge. Edge proxy kami meneruskan frame SSE begitu tiba — setiap chunk mereset timeout proxy, sehingga request streaming bertahan untuk completion sepanjang apa pun. Request non-streaming tunduk pada timeout proxy 100 detik; gunakan streaming untuk model yang berpikir lama (mis., R1, Opus).

Di mana streaming bekerja

Tiga endpoint mendukung streaming

Kirim stream:true pada salah satu endpoint ini dan gateway merespons dengan Server-Sent Events. Seluruh pipeline guardrail + metering berjalan identik di setiap jalur.

  • POST /v1/chat/completions — endpoint chat yang kompatibel dengan OpenAI. Chunk mengikuti format chat.completion.chunk milik OpenAI (ditampilkan di atas).
  • POST /anthropic/v1/messages — endpoint Anthropic Messages. Frame mengikuti format event Anthropic (message_start → content_block_delta → message_stop), bukan chunk OpenAI. Lihat /docs/endpoints/anthropic-messages.
  • POST /anoman/v1/playground/run — endpoint playground dashboard/studio yang menggerakkan alat compare 3 panel.

Format wire

Anatomi chunk

Setiap frame adalah satu baris: data: <json> diikuti satu baris kosong. Stream diakhiri dengan data: [DONE]. Chunk mengikuti bentuk chat.completion.chunk milik OpenAI, kecuali frame kedua dari terakhir adalah blok metadata Anoman.

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1709876543,"model":"claude-sonnet-4-6","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Once "},"finish_reason":null}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"upon a "},"finish_reason":null}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"time..."},"finish_reason":null}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":7,"completion_tokens":4,"total_tokens":11}}

data: {"_anoman":{"guardrails":{"injection":{"status":"pass","score":0.02}},"routing":{"mode":"realtime","region":"id"},"cache":{"hit":false},"cost_usd":"0.000041","weighted_tokens":44}}

data: [DONE]
  • Chunk pertama: delta.role: "assistant" dengan konten kosong.
  • Chunk perantara: delta.content token demi token.
  • Chunk completion terakhir: finish_reason terisi + usage total.
  • Frame kedua dari terakhir: {"_anoman": {...}} — hasil guardrail, region routing, cache hit, biaya, weighted token.
  • Terminator: data: [DONE].

Error di tengah stream

Ketika upstream putus di tengah jalan

Kegagalan pra-panggilan (auth, rate limit, blokir guardrail) mengembalikan error HTTP normal sebelum response SSE dibuka. Setelah streaming dimulai, status HTTP sudah 200 — jadi kegagalan upstream di tengah stream ditandai oleh frame error di dalam body stream:

data: {"error":{"type":"provider_error","code":"stream_interrupted","message":"upstream stream interrupted","_anoman":{"failure_class":"upstream_error","completion_tokens":42}}}

data: [DONE]

Klien Anda harus memeriksa setiap chunk yang di-parse untuk key error. Jika ada, perlakukan response sebagai gagal (jangan commit output parsial) dan terapkan logika retry normal Anda. SDK OpenAI otomatis melempar exception saat melihat frame ini.

Catatan konten parsial. Field _anoman.completion_tokens memberi tahu berapa banyak token penyelesaian yang dikeluarkan model sebelum kegagalan. Berguna untuk telemetri — dan untuk memutuskan apakah hasil parsial masih layak dipakai di UX Anda.

Penanganan SSE mentah

Tanpa SDK

Ketika Anda tidak bisa memakai SDK OpenAI/Anthropic — edge runtime, klien HTTP alternatif, proxy kustom — berikut parser SSE minimal:

// Useful when you need fine-grained control — e.g. piping straight
// to a WebSocket, or running in an environment without the OpenAI SDK.

const response = await fetch(
  "https://api.anoman.io/v1/chat/completions",
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.ANOMAN_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-6",
      messages: [{ role: "user", content: "Hi" }],
      stream: true,
    }),
  },
);

if (!response.ok || !response.body) {
  throw new Error(`HTTP ${response.status}`);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  // Split SSE messages on blank lines
  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";

  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const payload = line.slice(6).trim();
    if (payload === "[DONE]") return;
    const parsed = JSON.parse(payload);
    if (parsed.error) {
      throw new Error(parsed.error.message);
    }
    const delta = parsed.choices?.[0]?.delta?.content;
    if (delta) process.stdout.write(delta);
  }
}

Pembatalan

Menghentikan stream lebih awal

Menutup koneksi HTTP membatalkan panggilan upstream dalam ~50 ms. Token yang dihasilkan sebelum pembatalan tetap ditagih; dashboard mencatat pembatalan sebagai routing_mode: streaming_cancelled.

import time
from openai import OpenAI

client = OpenAI(base_url="https://api.anoman.io/v1", api_key="anm-sk-...")

stream = client.chat.completions.create(
    model="claude-sonnet-4-6",
    messages=[{"role": "user", "content": "Write a 5-paragraph essay"}],
    stream=True,
)

deadline = time.time() + 5  # cancel after 5 seconds
collected = []
for chunk in stream:
    delta = chunk.choices[0].delta.content or ""
    collected.append(delta)
    if time.time() > deadline:
        stream.close()  # closes the HTTP connection
        break

print("Collected:", "".join(collected))

Guardrail & metering

Identik dengan non-streaming

Streaming hanya mengubah cara byte sampai ke Anda — bukan apa yang gateway lakukan di sekitar panggilan. Setiap request melewati pipeline yang sama seperti request non-streaming.

  • Guardrail berjalan sebelum token pertama. Pemeriksaan pra-panggilan (injection, PII, moderasi konten, policy) dieksekusi sebelum stream dibuka. Request yang diblokir tidak pernah mulai streaming — ia mengembalikan error HTTP biasa (mis. 403), bukan body SSE.
  • Metering dicatat saat stream ditutup. Penggunaan token, weighted token, biaya, dan trace berasal dari usage final penyedia — angka yang sama seperti panggilan non-streaming. Stream yang dibatalkan tetap mencatat penggunaan parsial.
  • Error tidak membocorkan apa pun. Kegagalan penyedia di tengah stream memancarkan frame error generik (upstream_error) tanpa detail internal — pesan penyedia mentah hanya tersimpan di log server.

Yang tidak didukung dalam streaming

Keterbatasan yang diketahui

  • Batch routing — request streaming selalu memakai jalur real-time. Batch ditujukan untuk beban kerja non-interaktif di mana SLA 5–30 menit dapat diterima sebagai ganti biaya ~50%.
  • MCP per-tool RBAC — saat ini hanya diberlakukan pada response JSON. Jika alur kerja Anda butuh penolakan ketat per tool, panggil dengan stream: false sampai RBAC streaming dirilis.
  • Penulisan semantic cache terjadi saat stream ditutup, bukan per chunk. Cache hit memotong stream dan mengembalikan JSON (karena response penuh sudah diketahui).

Coba streaming di playground dashboard.

Bandingkan 3 panel dengan output streaming berdampingan.