AgentStack
SKILL verified MIT Self-run

Llm Streaming

skill-ultroncore-claude-skill-vault-llm-streaming · by UltronCore

>

No reviews yet
0 installs
2 views
0.0% view→install

Install

$ agentstack add skill-ultroncore-claude-skill-vault-llm-streaming

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Llm Streaming? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

LLM Streaming

When to Use

  • Streaming Claude or other LLM responses to the client
  • Building Next.js App Router API routes that stream
  • Creating React components that render streaming text
  • Handling text_delta events from the Anthropic SDK
  • Setting up SSE (Server-Sent Events) endpoints
  • Streaming responses that include tool use

Core Rules

  1. Always set the response Content-Type to text/event-stream for SSE; set Cache-Control: no-cache and Connection: keep-alive.
  2. Each SSE message must end with \n\n (double newline) — single newline is continuation, not a message boundary.
  3. Use TransformStream or ReadableStream in Next.js App Router — res.write() is Pages Router only.
  4. Flush the stream on each chunk — buffering defeats the purpose of streaming.
  5. Always handle stream errors and send a terminal event ([DONE] or a typed error event) so clients don't hang.
  6. When streaming with tool use, buffer tool input JSON until the input_json delta is complete — never parse partial JSON.
  7. Use AbortController on the client to cancel in-flight streams when the user navigates away.
  8. Token counts are only available at stream end via message_stop event — not during stream.
  9. In React, append chunks to state with a functional updater: setText(prev => prev + chunk) to avoid stale closure bugs.
  10. Test streams locally with curl -N (no buffering) before wiring to a frontend.

Anthropic SDK Streaming (Python)

import anthropic

client = anthropic.Anthropic()

# Method 1: stream() context manager (recommended)
with client.messages.stream(
    model="claude-opus-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Write a short story about a robot."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    # Access final message after stream ends
    final_message = stream.get_final_message()
    print(f"\n\nInput tokens: {final_message.usage.input_tokens}")

# Method 2: raw event iteration
with client.messages.stream(
    model="claude-opus-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}],
) as stream:
    for event in stream:
        if event.type == "content_block_delta":
            if event.delta.type == "text_delta":
                print(event.delta.text, end="", flush=True)
        elif event.type == "message_stop":
            print("\n[Stream complete]")

# Method 3: async streaming
import asyncio
import anthropic

async def stream_async():
    client = anthropic.AsyncAnthropic()
    async with client.messages.stream(
        model="claude-opus-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Tell me a joke."}],
    ) as stream:
        async for text in stream.text_stream:
            print(text, end="", flush=True)

asyncio.run(stream_async())

Anthropic SDK Streaming (TypeScript)

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

// Method 1: stream() with text helper
const stream = client.messages.stream({
  model: "claude-opus-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Write a haiku." }],
});

stream.on("text", (text) => {
  process.stdout.write(text);
});

const finalMessage = await stream.finalMessage();
console.log(`\nTokens used: ${finalMessage.usage.input_tokens}`);

// Method 2: async iteration over events
const stream2 = await client.messages.create({
  model: "claude-opus-4-5",
  max_tokens: 1024,
  stream: true,
  messages: [{ role: "user", content: "Hello!" }],
});

for await (const event of stream2) {
  if (
    event.type === "content_block_delta" &&
    event.delta.type === "text_delta"
  ) {
    process.stdout.write(event.delta.text);
  }
}

Next.js App Router SSE Route Handler

// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
import { NextRequest } from "next/server";

const client = new Anthropic();

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

  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      try {
        const anthropicStream = client.messages.stream({
          model: "claude-opus-4-5",
          max_tokens: 2048,
          system: system ?? "You are a helpful assistant.",
          messages,
        });

        anthropicStream.on("text", (text) => {
          // SSE format: "data: \n\n"
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({ text })}\n\n`)
          );
        });

        anthropicStream.on("error", (error) => {
          controller.enqueue(
            encoder.encode(
              `data: ${JSON.stringify({ error: error.message })}\n\n`
            )
          );
          controller.close();
        });

        await anthropicStream.finalMessage();
        controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
        controller.close();
      } catch (error) {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ error: String(error) })}\n\n`)
        );
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

React Streaming UI Hook

// hooks/useStreamingChat.ts
import { useState, useCallback, useRef } from "react";

interface Message {
  role: "user" | "assistant";
  content: string;
}

export function useStreamingChat() {
  const [messages, setMessages] = useState([]);
  const [streamingText, setStreamingText] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const abortRef = useRef(null);

  const sendMessage = useCallback(async (userMessage: string) => {
    const newMessages: Message[] = [
      ...messages,
      { role: "user", content: userMessage },
    ];
    setMessages(newMessages);
    setStreamingText("");
    setIsStreaming(true);

    // Cancel any existing stream
    abortRef.current?.abort();
    abortRef.current = new AbortController();

    try {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ messages: newMessages }),
        signal: abortRef.current.signal,
      });

      if (!response.body) throw new Error("No response body");

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

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

        const chunk = decoder.decode(value, { stream: true });
        // Parse SSE lines
        for (const line of chunk.split("\n")) {
          if (!line.startsWith("data: ")) continue;
          const data = line.slice(6);
          if (data === "[DONE]") break;

          try {
            const parsed = JSON.parse(data);
            if (parsed.text) {
              accumulated += parsed.text;
              // Functional updater avoids stale closure
              setStreamingText((prev) => prev + parsed.text);
            }
          } catch {
            // Ignore parse errors on incomplete chunks
          }
        }
      }

      // Commit streaming text to messages
      setMessages((prev) => [
        ...prev,
        { role: "assistant", content: accumulated },
      ]);
      setStreamingText("");
    } catch (error) {
      if ((error as Error).name !== "AbortError") {
        console.error("Stream error:", error);
      }
    } finally {
      setIsStreaming(false);
    }
  }, [messages]);

  const stop = useCallback(() => {
    abortRef.current?.abort();
    setIsStreaming(false);
  }, []);

  return { messages, streamingText, isStreaming, sendMessage, stop };
}

React Streaming Chat Component

// components/StreamingChat.tsx
"use client";
import { useStreamingChat } from "@/hooks/useStreamingChat";
import { useState } from "react";

export function StreamingChat() {
  const { messages, streamingText, isStreaming, sendMessage, stop } =
    useStreamingChat();
  const [input, setInput] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim() || isStreaming) return;
    sendMessage(input);
    setInput("");
  };

  return (
    
      
        {messages.map((msg, i) => (
          
            {msg.content}
          
        ))}
        {isStreaming && streamingText && (
          
            
              {streamingText}
              ▊
            
          
        )}
      

      
         setInput(e.target.value)}
          className="flex-1 border rounded-lg px-3 py-2"
          placeholder="Type a message..."
          disabled={isStreaming}
        />
        {isStreaming ? (
          
            Stop
          
        ) : (
          
            Send
          
        )}
      
    
  );
}

Python FastAPI Streaming Endpoint

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import anthropic
import json

app = FastAPI()
client = anthropic.Anthropic()

@app.post("/api/chat")
async def chat_stream(body: dict):
    messages = body.get("messages", [])

    def generate():
        with client.messages.stream(
            model="claude-opus-4-5",
            max_tokens=2048,
            messages=messages,
        ) as stream:
            for text in stream.text_stream:
                # SSE format
                yield f"data: {json.dumps({'text': text})}\n\n"
            yield "data: [DONE]\n\n"

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",  # Disable nginx buffering
        },
    )

Streaming with Tool Use

# Buffer tool input JSON — never parse partial JSON
tool_input_buffer = {}

with client.messages.stream(
    model="claude-opus-4-5",
    max_tokens=2048,
    tools=tools,
    messages=messages,
) as stream:
    current_tool_use_id = None
    current_tool_name = None
    input_json_buffer = ""

    for event in stream:
        if event.type == "content_block_start":
            if event.content_block.type == "tool_use":
                current_tool_use_id = event.content_block.id
                current_tool_name = event.content_block.name
                input_json_buffer = ""

        elif event.type == "content_block_delta":
            if event.delta.type == "text_delta":
                print(event.delta.text, end="", flush=True)
            elif event.delta.type == "input_json_delta":
                # Buffer — do NOT parse yet
                input_json_buffer += event.delta.partial_json

        elif event.type == "content_block_stop":
            if current_tool_use_id and input_json_buffer:
                # Now safe to parse complete JSON
                tool_input = json.loads(input_json_buffer)
                print(f"\n[Tool call] {current_tool_name}: {tool_input}")
                current_tool_use_id = None
                input_json_buffer = ""

SSE Event Format Reference

# Standard SSE format:
data: {"text": "Hello"}\n\n

# With event type:
event: message\n
data: {"text": "Hello"}\n\n

# Keepalive (comment):
: ping\n\n

# Terminal signal:
data: [DONE]\n\n

# Multi-line data (rare):
data: line one\n
data: line two\n\n

Token Counting During Stream

# Tokens are only available at stream end
with client.messages.stream(...) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

    # Get final usage AFTER stream completes
    final = stream.get_final_message()
    print(f"\nInput: {final.usage.input_tokens} | Output: {final.usage.output_tokens}")

Buffering Strategy for Smooth UI

// Throttle state updates to avoid re-render thrashing
let buffer = "";
let rafId: number | null = null;

function flushBuffer() {
  setStreamingText((prev) => prev + buffer);
  buffer = "";
  rafId = null;
}

// In your stream reader loop:
buffer += chunk;
if (!rafId) {
  rafId = requestAnimationFrame(flushBuffer);
}

Debugging Streams

# Test your SSE endpoint without a browser
curl -N -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Hello"}]}'

# -N disables curl buffering so you see chunks as they arrive

Related Skills

  • claude-api-skill — streaming with Claude
  • websocket-realtime — streaming over WebSockets
  • streaming-llm-responses — response streaming patterns

GitNexus Index

This skill is indexed by GitNexus for knowledge graph traversal. Index path: /Users/localuser/.claude/skills/llm-streaming/.gitnexus Last indexed: 2026-05-23

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.