# ストリーミングチャット UI

Source: https://aiand-docs-staging.pages.dev/ja/cookbook/streaming-ui/

ストリーミング UI は、トークンが到着するたびに表示されるため即時感があります。パターンは共通: サーバルートが ai& にストリームを開き、SSE のボディをそのままブラウザに流し、クライアントが delta を描画します。

## サーバルート (Next.js App Router)

```ts
// app/api/chat/route.ts
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.aiand.com/v1",
  apiKey: process.env.AIAND_API_KEY!,
});

export async function POST(req: Request) {
  const { messages } = await req.json();

  const stream = await client.chat.completions.create({
    model: "google/gemma-4-31b-it",
    messages,
    stream: true,
    stream_options: { include_usage: true },
  });

  const encoder = new TextEncoder();
  const body = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        const delta = chunk.choices[0]?.delta?.content ?? "";
        if (delta) controller.enqueue(encoder.encode(delta));
      }
      controller.close();
    },
  });

  return new Response(body, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}
```

## クライアントコンポーネント

```tsx
"use client";
import { useState } from "react";

export default function Chat() {
  const [input, setInput] = useState("");
  const [output, setOutput] = useState("");

  async function send() {
    setOutput("");
    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: [{ role: "user", content: input }] }),
    });
    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      setOutput((prev) => prev + decoder.decode(value));
    }
  }

  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={send}>Send</button>
      <pre>{output}</pre>
    </div>
  );
}
```

## コストとリクエスト ID の取得

Node SDK の `for await` ループはコンテンツチャンクのみを iterate し、`metrics` trailer イベントを直接公開しません。コストとリクエスト ID を取得するには:

- リクエスト ID はレスポンスヘッダ `X-Request-ID` から読む。
- コストはリクエストヘッダ `X-Aiand-Metrics: true` を付与した上で、`fetch` + SSE パーサで `data: [DONE]` の後の `event: metrics` を読む — ペイロードにはトークン数・`cost`・`currency` が含まれます。[ストリーミングイベント](/ja/reference/streaming-events/) を参照。

## ポイント

- アップストリームのレスポンスボディは OpenAI の形式と完全に一致するため、ベース URL を切り替えるだけで既存パーサがそのまま動作します。
- API キーをクライアントコードに埋め込まず、必ずサーバ経由で呼び出します。
- Vercel AI SDK の `useChat` 連携については [Vercel AI SDK](/ja/integrations/vercel-ai/) を参照。

<Aside type="caution">
  `metrics` イベントは `data: [DONE]` の **後** に届きます。初回チャンクで接続を閉じると取りこぼします。
</Aside>
