> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arouter.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 스트리밍

> Server-Sent Events (SSE)를 사용하여 실시간 토큰별 스트리밍을 활성화합니다. ARouter는 모든 주요 제공업체의 스트리밍 응답을 지원합니다.

ARouter는 모든 모델에 대해 스트리밍 응답을 지원합니다. 스트리밍이 활성화되면 Token이 생성될 때 실시간으로 전달됩니다.

스트리밍을 활성화하려면 요청 본문에 `stream: true`를 설정하세요.

<Tabs>
  <Tab title="Python (OpenAI)">
    ```python theme={null}
    from openai import OpenAI

    client = OpenAI(
        base_url="https://api.arouter.ai/v1",
        api_key="lr_live_xxxx",
    )

    stream = client.chat.completions.create(
        model="openai/gpt-5.4",
        messages=[{"role": "user", "content": "How would you build the tallest building ever?"}],
        stream=True,
    )

    for chunk in stream:
        content = chunk.choices[0].delta.content
        if content:
            print(content, end="", flush=True)

    # Final chunk includes usage stats
    # Access via: stream.get_final_completion().usage
    ```
  </Tab>

  <Tab title="Node.js (OpenAI)">
    ```typescript theme={null}
    import OpenAI from "openai";

    const client = new OpenAI({
      baseURL: "https://api.arouter.ai/v1",
      apiKey: "lr_live_xxxx",
    });

    const stream = await client.chat.completions.create({
      model: "openai/gpt-5.4",
      messages: [{ role: "user", content: "How would you build the tallest building ever?" }],
      stream: true,
    });

    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) process.stdout.write(content);

      // Final chunk includes usage stats
      if (chunk.usage) {
        console.log("\nUsage:", chunk.usage);
      }
    }
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    stream, err := client.ChatCompletionStream(ctx, arouter.ChatCompletionRequest{
        Model: "openai/gpt-5.4",
        Messages: []arouter.Message{
            {Role: "user", Content: "How would you build the tallest building ever?"},
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    defer stream.Close()

    for {
        chunk, err := stream.Recv()
        if err == arouter.ErrStreamDone {
            break
        }
        if err != nil {
            log.Fatal(err)
        }
        fmt.Print(chunk.Choices[0].Delta.Content)
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -N https://api.arouter.ai/v1/chat/completions \
      -H "Authorization: Bearer lr_live_xxxx" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-5.4",
        "messages": [{"role": "user", "content": "How would you build the tallest building ever?"}],
        "stream": true
      }'
    ```
  </Tab>

  <Tab title="fetch (raw)">
    ```typescript theme={null}
    const response = await fetch('https://api.arouter.ai/v1/chat/completions', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer lr_live_xxxx',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'openai/gpt-5.4',
        messages: [{ role: 'user', content: 'How would you build the tallest building ever?' }],
        stream: true,
      }),
    });

    const reader = response.body?.getReader();
    if (!reader) throw new Error('No response body');

    const decoder = new TextDecoder();
    let buffer = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        while (true) {
          const lineEnd = buffer.indexOf('\n');
          if (lineEnd === -1) break;

          const line = buffer.slice(0, lineEnd).trim();
          buffer = buffer.slice(lineEnd + 1);

          if (line.startsWith('data: ')) {
            const data = line.slice(6);
            if (data === '[DONE]') break;

            try {
              const parsed = JSON.parse(data);
              const content = parsed.choices[0]?.delta?.content;
              if (content) process.stdout.write(content);
            } catch (e) {
              // ignore invalid JSON
            }
          }
        }
      }
    } finally {
      reader.cancel();
    }
    ```
  </Tab>
</Tabs>

## Anthropic 스트리밍

Anthropic SDK는 자체 스트리밍 형식을 사용합니다：

```python theme={null}
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.arouter.ai",
    api_key="lr_live_xxxx",
)

with client.messages.stream(
    model="claude-sonnet-4.6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "How would you build the tallest building ever?"}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
```

## Gemini 스트리밍

Gemini는 `generateContent` 대신 `streamGenerateContent`를 사용합니다：

```python theme={null}
import google.generativeai as genai

genai.configure(
    api_key="lr_live_xxxx",
    transport="rest",
    client_options={"api_endpoint": "https://api.arouter.ai"},
)

model = genai.GenerativeModel("gemini-2.5-flash")
response = model.generate_content("How would you build the tallest building ever?", stream=True)

for chunk in response:
    print(chunk.text, end="", flush=True)
```

## SSE 형식

내부적으로 스트리밍은 Server-Sent Events를 사용합니다. 각 콘텐츠 이벤트는 다음과 같습니다：

```
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"openai/gpt-5.4","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"openai/gpt-5.4","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"openai/gpt-5.4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
```

`[DONE]` 직전의 마지막 청크에는 빈 `choices` 배열과 함께 사용량 데이터가 포함됩니다：

```
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"openai/gpt-5.4","choices":[],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}}

data: [DONE]
```

<Note>
  ARouter는 연결 타임아웃을 방지하기 위해 SSE 주석(`:` 으로 시작하는 줄)을 간헐적으로 전송할 수 있습니다. SSE 사양에 따라 이러한 주석은 안전하게 무시할 수 있습니다.
</Note>

### 권장 SSE 클라이언트 라이브러리

일부 SSE 클라이언트 구현은 페이로드를 올바르게 파싱하지 못할 수 있습니다. 다음을 권장합니다：

* [eventsource-parser](https://github.com/rexxars/eventsource-parser) — 경량 SSE 파서
* [OpenAI SDK](https://www.npmjs.com/package/openai) — SSE, 도구 호출, 사용량을 자동 처리
* [Vercel AI SDK](https://www.npmjs.com/package/ai) — React/Next.js 스트리밍 헬퍼

## 스트림 취소

스트리밍 요청은 연결을 중단하여 취소할 수 있습니다. 지원되는 제공업체의 경우 즉시 모델 처리가 중지됩니다.

<Tabs>
  <Tab title="Node.js (AbortController)">
    ```typescript theme={null}
    import OpenAI from "openai";

    const client = new OpenAI({
      baseURL: "https://api.arouter.ai/v1",
      apiKey: "lr_live_xxxx",
    });

    const controller = new AbortController();

    try {
      const stream = await client.chat.completions.create(
        {
          model: "openai/gpt-5.4",
          messages: [{ role: "user", content: "Write a long story" }],
          stream: true,
        },
        { signal: controller.signal },
      );

      for await (const chunk of stream) {
        const content = chunk.choices[0]?.delta?.content;
        if (content) process.stdout.write(content);
      }
    } catch (error) {
      if (error.name === "AbortError") {
        console.log("Stream cancelled");
      } else {
        throw error;
      }
    }

    // To cancel:
    controller.abort();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests
    from threading import Event, Thread

    def stream_with_cancellation(prompt: str, cancel_event: Event):
        with requests.Session() as session:
            response = session.post(
                "https://api.arouter.ai/v1/chat/completions",
                headers={"Authorization": "Bearer lr_live_xxxx"},
                json={
                    "model": "openai/gpt-5.4",
                    "messages": [{"role": "user", "content": prompt}],
                    "stream": True,
                },
                stream=True,
            )
            try:
                for line in response.iter_lines():
                    if cancel_event.is_set():
                        response.close()
                        return
                    if line:
                        print(line.decode(), end="", flush=True)
            finally:
                response.close()

    cancel_event = Event()
    t = Thread(target=lambda: stream_with_cancellation("Write a long story", cancel_event))
    t.start()

    # To cancel:
    cancel_event.set()
    ```
  </Tab>

  <Tab title="fetch (AbortController)">
    ```typescript theme={null}
    const controller = new AbortController();

    try {
      const response = await fetch('https://api.arouter.ai/v1/chat/completions', {
        method: 'POST',
        headers: {
          Authorization: 'Bearer lr_live_xxxx',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: 'openai/gpt-5.4',
          messages: [{ role: 'user', content: 'Write a long story' }],
          stream: true,
        }),
        signal: controller.signal,
      });

      // process stream...
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Stream cancelled');
      } else {
        throw error;
      }
    }

    // To cancel:
    controller.abort();
    ```
  </Tab>
</Tabs>

## 스트리밍 중 오류 처리

ARouter는 스트리밍 프로세스에서 오류가 발생하는 시점에 따라 다르게 처리합니다.

### 토큰 전송 전 오류

토큰이 스트리밍되기 전에 오류가 발생하면, ARouter는 적절한 HTTP 상태 코드와 함께 표준 JSON 오류 응답을 반환합니다：

```json theme={null}
{
  "error": {
    "code": 400,
    "message": "Invalid model specified"
  }
}
```

일반적인 HTTP 상태 코드：

| 코드  | 의미                                   |
| --- | ------------------------------------ |
| 400 | Bad Request — 잘못된 파라미터               |
| 401 | Unauthorized — 잘못된 API key           |
| 402 | Payment Required — 크레딧 부족            |
| 429 | Too Many Requests — 속도 제한            |
| 502 | Bad Gateway — 제공업체 오류                |
| 503 | Service Unavailable — 사용 가능한 제공업체 없음 |

### 토큰 전송 후 오류 (스트림 도중)

일부 토큰이 이미 스트리밍된 후에 오류가 발생하면, ARouter는 HTTP 상태 코드를 변경할 수 없습니다(이미 200 OK). 오류는 SSE 이벤트로 전송됩니다：

```
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"openai/gpt-5.4","error":{"code":"server_error","message":"Provider disconnected unexpectedly"},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}
```

주요 특징：

* 오류는 표준 응답 필드와 함께 최상위 레벨에 표시됨
* 스트림을 종료하기 위해 `finish_reason: "error"`를 포함한 `choices` 배열이 포함됨
* 헤더가 이미 전송되었으므로 HTTP 상태는 200 OK 유지

### 오류 처리 코드 예제

<Tabs>
  <Tab title="Python (OpenAI)">
    ```python theme={null}
    from openai import OpenAI, APIStatusError

    client = OpenAI(
        base_url="https://api.arouter.ai/v1",
        api_key="lr_live_xxxx",
    )

    try:
        stream = client.chat.completions.create(
            model="openai/gpt-5.4",
            messages=[{"role": "user", "content": "Write a story"}],
            stream=True,
        )
        for chunk in stream:
            content = chunk.choices[0].delta.content
            if content:
                print(content, end="", flush=True)
    except APIStatusError as e:
        print(f"\nError {e.status_code}: {e.message}")
    ```
  </Tab>

  <Tab title="Node.js (OpenAI)">
    ```typescript theme={null}
    import OpenAI from "openai";

    const client = new OpenAI({
      baseURL: "https://api.arouter.ai/v1",
      apiKey: "lr_live_xxxx",
    });

    try {
      const stream = await client.chat.completions.create({
        model: "openai/gpt-5.4",
        messages: [{ role: "user", content: "Write a story" }],
        stream: true,
      });

      for await (const chunk of stream) {
        // Check for mid-stream errors
        if ("error" in chunk) {
          console.error(`Stream error: ${(chunk as any).error.message}`);
          if (chunk.choices?.[0]?.finish_reason === "error") {
            console.log("Stream terminated due to error");
          }
          break;
        }
        const content = chunk.choices[0]?.delta?.content;
        if (content) process.stdout.write(content);
      }
    } catch (error) {
      if (error instanceof OpenAI.APIError) {
        console.error(`Error ${error.status}: ${error.message}`);
      } else {
        throw error;
      }
    }
    ```
  </Tab>

  <Tab title="fetch (raw)">
    ```typescript theme={null}
    async function streamWithErrorHandling(prompt: string) {
      const response = await fetch('https://api.arouter.ai/v1/chat/completions', {
        method: 'POST',
        headers: {
          Authorization: 'Bearer lr_live_xxxx',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: 'openai/gpt-5.4',
          messages: [{ role: 'user', content: prompt }],
          stream: true,
        }),
      });

      // Check initial HTTP status for pre-stream errors
      if (!response.ok) {
        const error = await response.json();
        console.error(`Error: ${error.error.message}`);
        return;
      }

      const reader = response.body?.getReader();
      if (!reader) throw new Error('No response body');
      const decoder = new TextDecoder();
      let buffer = '';

      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          buffer += decoder.decode(value, { stream: true });

          while (true) {
            const lineEnd = buffer.indexOf('\n');
            if (lineEnd === -1) break;
            const line = buffer.slice(0, lineEnd).trim();
            buffer = buffer.slice(lineEnd + 1);

            if (line.startsWith('data: ')) {
              const data = line.slice(6);
              if (data === '[DONE]') return;

              try {
                const parsed = JSON.parse(data);

                // Check for mid-stream error
                if (parsed.error) {
                  console.error(`Stream error: ${parsed.error.message}`);
                  return;
                }

                const content = parsed.choices[0]?.delta?.content;
                if (content) process.stdout.write(content);
              } catch (e) {
                // ignore parse errors
              }
            }
          }
        }
      } finally {
        reader.cancel();
      }
    }
    ```
  </Tab>
</Tabs>
