anoman
MasukDapatkan API Key
Beranda Dokumentasi

Streaming

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.

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.

Python — OpenAI SDK

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)

TypeScript — OpenAI SDK

import OpenAI from "openai";
 
const client = new OpenAI({
  baseURL: "https://api.anoman.io/v1",
  apiKey: process.env.ANOMAN_API_KEY,
});
 
const stream = await client.chat.completions.create({
  model: "claude-sonnet-4-6",
  messages: [{ role: "user", content: "Tell me a short story" }],
  stream: true,
});
 
for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta?.content ?? "";
  process.stdout.write(delta);
}

cURL

# Use -N to disable buffering — see chunks as they arrive
curl -N https://api.anoman.io/v1/chat/completions \
  -H "Authorization: Bearer anm-sk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4-6",
    "messages": [{"role": "user", "content": "Tell me a short story"}],
    "stream": 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).

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.

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.

Format wire — contoh chunk

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].

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:

Frame error di tengah 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.

Tanpa SDK

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

TypeScript — raw fetch (tanpa SDK)

// 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);
  }
}

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.

Python — batalkan di tengah stream

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))

TypeScript — batalkan via AbortController

const controller = new AbortController();
 
setTimeout(() => controller.abort(), 5000);  // cancel after 5s
 
try {
  const stream = await client.chat.completions.create(
    {
      model: "claude-sonnet-4-6",
      messages: [{ role: "user", content: "Write a 5-paragraph essay" }],
      stream: true,
    },
    { signal: controller.signal },
  );
  for await (const chunk of stream) {
    process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
  }
} catch (e) {
  if ((e as Error).name === "AbortError") {
    console.log("\n[cancelled]");
  } else {
    throw e;
  }
}

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.

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.

On this page