# Response Chunking Strategies

**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability

---

## Introduksjon

Response chunking handler om hvordan store AI-modellresponser fra Azure OpenAI brytes opp og leveres til klienter. Det finnes to hovedtilnærminger: streaming via Server-Sent Events (SSE) der modellens output leveres token-for-token i sanntid, og chunking av store responser der output deles opp i semantisk meningsfulle blokker for videre prosessering.

Streaming er den mest brukte chunking-strategien for Azure OpenAI. Når `stream: true` settes i API-kallet, returnerer tjenesten delta-oppdateringer som Server-Sent Events ettersom tokens genereres. Dette gir brukeren umiddelbar feedback (time-to-first-token typisk 200-500ms) i stedet for å vente på hele responsen (som kan ta 5-30 sekunder for lange output). For programmatisk prosessering der hele responsen trengs, er chunking av det endelige resultatet i semantisk koherente blokker viktig for downstream-systemer.

For norsk offentlig sektor der AI brukes til å generere lange dokumenter (saksframlegg, utredninger, rapporter), er response chunking avgjørende for å levere god brukeropplevelse og for å kunne prosessere store responser effektivt i saksbehandlingssystemer.

## Kjernekomponenter

| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Server-Sent Events (SSE) | Real-time streaming av tokens | HTTP SSE |
| stream_options | Konfigurer streaming-oppførsel | Azure OpenAI API |
| Application Gateway | SSE proxy og load balancing | Azure App Gateway |
| API Management | SSE-støtte med policy-basert routing | Azure APIM |
| SignalR | Real-time push til web-klienter | Azure SignalR |

## Streaming med Server-Sent Events

### Python streaming-implementasjon

```python
from openai import AzureOpenAI
import sys

client = AzureOpenAI(
    azure_endpoint="https://my-aoai.openai.azure.com",
    api_key="...",
    api_version="2024-10-21"
)

def stream_chat_completion(messages: list[dict], model: str = "gpt-4o"):
    """Stream response with real-time token delivery."""
    collected_content = []

    stream = client.chat.completions.create(
        model=model,
        messages=messages,
        stream=True,
        stream_options={"include_usage": True},  # Få token-bruk til slutt
        max_tokens=2000
    )

    for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            token = chunk.choices[0].delta.content
            collected_content.append(token)
            sys.stdout.write(token)
            sys.stdout.flush()

        # Siste chunk inneholder usage
        if hasattr(chunk, 'usage') and chunk.usage:
            return {
                "content": "".join(collected_content),
                "prompt_tokens": chunk.usage.prompt_tokens,
                "completion_tokens": chunk.usage.completion_tokens,
                "total_tokens": chunk.usage.total_tokens
            }

    return {"content": "".join(collected_content)}


# Asynkron streaming
async def async_stream_completion(
    client: AsyncAzureOpenAI,
    messages: list[dict],
    model: str = "gpt-4o",
    on_token: callable = None
):
    """Async stream with callback per token."""
    chunks = []

    async with client.chat.completions.create(
        model=model,
        messages=messages,
        stream=True,
        stream_options={"include_usage": True}
    ) as stream:
        async for chunk in stream:
            if chunk.choices and chunk.choices[0].delta.content:
                token = chunk.choices[0].delta.content
                chunks.append(token)
                if on_token:
                    await on_token(token)

    return "".join(chunks)
```

### .NET streaming med IAsyncEnumerable

```csharp
using Azure.AI.OpenAI;
using OpenAI.Chat;

public class StreamingService
{
    private readonly AzureOpenAIClient _client;

    public async IAsyncEnumerable<string> StreamCompletionAsync(
        string deploymentName,
        IList<ChatMessage> messages,
        int maxTokens = 2000)
    {
        var chatClient = _client.GetChatClient(deploymentName);

        var options = new ChatCompletionOptions
        {
            MaxOutputTokenCount = maxTokens
        };

        // Stream deltas
        await foreach (var update in
            chatClient.CompleteChatStreamingAsync(messages, options))
        {
            foreach (var part in update.ContentUpdate)
            {
                if (!string.IsNullOrEmpty(part.Text))
                {
                    yield return part.Text;
                }
            }
        }
    }

    // Bruk i ASP.NET controller
    public async Task StreamToClient(
        HttpContext context,
        string deploymentName,
        IList<ChatMessage> messages)
    {
        context.Response.ContentType = "text/event-stream";
        context.Response.Headers.Append("Cache-Control", "no-cache");
        context.Response.Headers.Append("Connection", "keep-alive");

        var writer = new StreamWriter(context.Response.Body);

        await foreach (var token in StreamCompletionAsync(
            deploymentName, messages))
        {
            await writer.WriteAsync($"data: {token}\n\n");
            await writer.FlushAsync();
        }

        await writer.WriteAsync("data: [DONE]\n\n");
        await writer.FlushAsync();
    }
}
```

## Semantic Chunking Approaches

### Chunk store responser i meningsfulle blokker

```python
import re
from dataclasses import dataclass

@dataclass
class SemanticChunk:
    index: int
    content: str
    chunk_type: str  # "heading", "paragraph", "code", "list", "table"
    token_count: int

def semantic_chunk_response(
    response_text: str,
    max_chunk_tokens: int = 500,
    model: str = "gpt-4o"
) -> list[SemanticChunk]:
    """Split AI response into semantically coherent chunks."""
    import tiktoken
    enc = tiktoken.encoding_for_model(model)

    chunks = []
    current_chunk = []
    current_tokens = 0
    chunk_type = "paragraph"

    # Del på naturlige grenser
    lines = response_text.split('\n')

    for line in lines:
        line_tokens = len(enc.encode(line))

        # Identifiser chunk-type
        if line.startswith('#'):
            chunk_type = "heading"
        elif line.startswith('```'):
            chunk_type = "code"
        elif line.startswith('- ') or line.startswith('* '):
            chunk_type = "list"
        elif line.startswith('|'):
            chunk_type = "table"
        else:
            chunk_type = "paragraph"

        # Ny chunk ved heading eller ved token-grense
        if (line.startswith('#') and current_chunk) or \
           (current_tokens + line_tokens > max_chunk_tokens and current_chunk):
            chunks.append(SemanticChunk(
                index=len(chunks),
                content='\n'.join(current_chunk),
                chunk_type=chunk_type,
                token_count=current_tokens
            ))
            current_chunk = []
            current_tokens = 0

        current_chunk.append(line)
        current_tokens += line_tokens

    # Siste chunk
    if current_chunk:
        chunks.append(SemanticChunk(
            index=len(chunks),
            content='\n'.join(current_chunk),
            chunk_type=chunk_type,
            token_count=current_tokens
        ))

    return chunks
```

### Streaming accumulator med chunk-deteksjon

```python
class StreamingChunkAccumulator:
    """Accumulate streaming tokens into semantic chunks."""

    def __init__(
        self,
        on_chunk_complete: callable = None,
        chunk_boundary_pattern: str = r'\n#{1,3}\s'
    ):
        self.buffer = []
        self.chunks = []
        self.on_chunk_complete = on_chunk_complete
        self.boundary_pattern = re.compile(chunk_boundary_pattern)

    async def feed_token(self, token: str):
        """Feed a streaming token to the accumulator."""
        self.buffer.append(token)

        # Sjekk om vi har nådd en chunk-grense
        current_text = ''.join(self.buffer)
        if self.boundary_pattern.search(current_text):
            # Del på grensen
            parts = self.boundary_pattern.split(current_text, maxsplit=1)
            if len(parts) > 1:
                completed = parts[0]
                remaining = current_text[len(completed):]

                if completed.strip():
                    chunk = SemanticChunk(
                        index=len(self.chunks),
                        content=completed.strip(),
                        chunk_type=self._detect_type(completed),
                        token_count=len(completed.split())  # Estimat
                    )
                    self.chunks.append(chunk)

                    if self.on_chunk_complete:
                        await self.on_chunk_complete(chunk)

                self.buffer = [remaining]

    def finalize(self) -> list[SemanticChunk]:
        """Finalize and return all chunks."""
        remaining = ''.join(self.buffer).strip()
        if remaining:
            self.chunks.append(SemanticChunk(
                index=len(self.chunks),
                content=remaining,
                chunk_type=self._detect_type(remaining),
                token_count=len(remaining.split())
            ))
        return self.chunks

    def _detect_type(self, text: str) -> str:
        if text.startswith('```'):
            return "code"
        if text.startswith('#'):
            return "heading"
        if text.startswith('- ') or text.startswith('* '):
            return "list"
        return "paragraph"
```

## Client-Side Reassembly

### Web-klient med progressiv rendering

```typescript
// TypeScript: Client-side SSE consumption with chunk assembly
interface StreamChunk {
  content: string;
  isComplete: boolean;
  tokenCount: number;
}

class AIResponseAssembler {
  private chunks: string[] = [];
  private onUpdate: (text: string) => void;
  private onComplete: (text: string, stats: object) => void;

  constructor(
    onUpdate: (text: string) => void,
    onComplete: (text: string, stats: object) => void
  ) {
    this.onUpdate = onUpdate;
    this.onComplete = onComplete;
  }

  async streamFromEndpoint(url: string, body: object): Promise<void> {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...body, stream: true }),
    });

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

    const reader = response.body
      .pipeThrough(new TextDecoderStream())
      .getReader();

    let fullText = '';
    let buffer = '';

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

      buffer += value;
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6);
          if (data === '[DONE]') {
            this.onComplete(fullText, {
              totalChunks: this.chunks.length,
              totalLength: fullText.length
            });
            return;
          }

          try {
            const parsed = JSON.parse(data);
            const token = parsed.choices?.[0]?.delta?.content || '';
            if (token) {
              fullText += token;
              this.chunks.push(token);
              this.onUpdate(fullText);
            }
          } catch { /* skip malformed */ }
        }
      }
    }
  }
}
```

## Error Handling in Chunks

### Robust feilhåndtering for streaming

```python
class ResilientStreamProcessor:
    """Handle errors during streaming response."""

    def __init__(self, client: AsyncAzureOpenAI, max_retries: int = 3):
        self.client = client
        self.max_retries = max_retries

    async def stream_with_recovery(
        self,
        messages: list[dict],
        model: str = "gpt-4o",
        max_tokens: int = 2000
    ) -> dict:
        """Stream with automatic recovery on failure."""
        accumulated = []
        total_tokens_generated = 0

        for attempt in range(self.max_retries):
            try:
                stream = await self.client.chat.completions.create(
                    model=model,
                    messages=messages,
                    stream=True,
                    max_tokens=max_tokens - total_tokens_generated
                )

                async for chunk in stream:
                    if chunk.choices and chunk.choices[0].delta.content:
                        token = chunk.choices[0].delta.content
                        accumulated.append(token)
                        total_tokens_generated += 1

                    # Sjekk for finish_reason
                    if chunk.choices and chunk.choices[0].finish_reason:
                        return {
                            "content": "".join(accumulated),
                            "finish_reason": chunk.choices[0].finish_reason,
                            "attempts": attempt + 1,
                            "recovered": attempt > 0
                        }

                # Stream fullført uten finish_reason
                return {
                    "content": "".join(accumulated),
                    "finish_reason": "stop",
                    "attempts": attempt + 1,
                    "recovered": attempt > 0
                }

            except Exception as e:
                if attempt < self.max_retries - 1:
                    # Fortsett fra der vi stoppet
                    partial = "".join(accumulated)
                    if partial:
                        # Legg til partial output som assistant-melding
                        messages = messages + [
                            {"role": "assistant", "content": partial},
                            {"role": "user", "content": "Fortsett fra der du stoppet."}
                        ]
                    await asyncio.sleep(2 ** attempt)
                else:
                    return {
                        "content": "".join(accumulated),
                        "finish_reason": "error",
                        "error": str(e),
                        "attempts": attempt + 1
                    }
```

## Norsk offentlig sektor

- **Universell utforming**: Streaming gir bedre brukeropplevelse for skjermlesere og sakte nettverk — bruker ser innhold progressivt i stedet for å vente.
- **Saksbehandlingssystemer**: Chunk store AI-responser i semantiske blokker (overskrifter, avsnitt, tabeller) for enkel integrasjon i saksbehandlingsdokumenter.
- **Logging og audit**: Ved streaming, logg den komplette responsen etter fullføring for arkiverings- og revisjonskrav.
- **Application Gateway**: Konfigurer response buffer disabled for SSE-støtte gjennom Azure Application Gateway eller API Management.

## Beslutningsrammeverk

| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Interaktiv chat UI | SSE streaming | Umiddelbar bruker-feedback |
| Batch dokumentprosessering | Ikke-streaming + semantic chunking | Enklere feilhåndtering |
| API-til-API integrasjon | Ikke-streaming | Enklere å parse komplett respons |
| Lang respons (>2000 tokens) | Streaming + chunk accumulator | Reduser opplevd ventetid |
| Kritisk pålitelighet | Streaming med recovery | Gjenoppta ved feil |

## Referanser

- [Azure OpenAI streaming](https://learn.microsoft.com/azure/foundry/openai/how-to/responses) — Streaming API
- [Server-Sent Events with Application Gateway](https://learn.microsoft.com/azure/application-gateway/use-server-sent-events) — SSE proxy
- [API Management SSE configuration](https://learn.microsoft.com/azure/api-management/how-to-server-sent-events) — APIM SSE
- [Server-Sent Events with App Gateway for Containers](https://learn.microsoft.com/azure/application-gateway/for-containers/server-sent-events) — Container SSE

## For Cosmo

- **Bruk denne referansen** når kunden implementerer streaming i AI-applikasjoner, trenger å chunke store responser, eller har feilhåndteringsproblemer med SSE.
- Streaming er alltid anbefalt for brukervendte applikasjoner — time-to-first-token reduseres fra sekunder til millisekunder.
- Konfigurer `stream_options: { include_usage: true }` for å få token-bruk i siste chunk — uten dette mangler kostnadssporing.
- Ved bruk av Application Gateway eller API Management som proxy: deaktiver response buffering for SSE-kompatibilitet.
- Implementer alltid recovery-logikk for streaming — nettverksavbrudd er uunngåelig i produksjon, og delvis generert output bør gjenbrukes.
