Ordre 20260912T193441Z-7358817909. Steg 1 var ikke transformen, men å rette
roadmapens R13-gate og få den ratifisert. Gaten `grep -rl "Cosmo"
skills/*/references -> 0` var usann på to uavhengige måter:
1. Ordren fanget den første: 451 av forekomstene er Azure Cosmos DB, ekte
produktinnhold. Diskriminatoren er ikke bokstaven «s» — `Cosmos <norsk
substantiv>` er genitiv av personaen (`### Cosmos tonalitet`), mens
`Cosmos DB`/`CosmosClient`/`cosmos_ru` er produkt.
2. Denne økten fant den andre: 132 persona-forekomster ligger i prosa,
tabeller, dialog-replikker og proveniens-linjer. Heading-nøytralisering
kan ikke nå dem, så «0 persona» er uoppnåelig også under den ratifiserte
formen. Operatøren ratifiserte alternativ A: gaten speiler formen, og de
132 bokføres til R13b/R14.
Tre korreksjoner av premisser som sto i ordren og STATE:
«ca 320 produkt» -> 451 (case-sensitivt nett manglet 327 lowercase
TOC-ankre + 99 identifikatorer; sann nevner 1 638)
«169 headinger» -> 401. 169 var `^## For Cosmo`-prefikset (168) og var
internt inkonsistent med sin egen topp-variant (204)
«417 matcher ingen
populasjon» -> 417 er cosmo-headinger utenfor kodefences; briefens
nevner var reell hele tiden
Fence-bevissthet er målt skadelig, ikke nødvendig: begge toggle-regler er
gale på dette korpuset (naiv toggle skjuler en ekte heading i
chain-of-thought-prompting.md, CommonMark-regelen ubalanserer
service-level-documentation-dr.md). Fence-agnostisk deteksjon finner 401
heading-linjer i nøyaktig de samme 40 variantene som fence-bevisst finner
400 i — ingen kodeblokk-linje er byte-identisk til en persona-heading. Derfor
nøkles transformen på 40 enumererte heading-tekster og ignorerer fences. En
ukjent variant kaster; en slug-kollisjon kaster. Ingenting auto-fikses.
TOC-en regenereres ikke, den rettes kirurgisk: alle 327 persona-lenker hadde
lenketekst lik én av de 40 heading-tekstene og anker lik slugify av den
(327/327, 0 avvik), så heading og TOC-entry skrives i samme operasjon og
ingen mellomtilstand etterlater en død lenke.
Ratifisert målform: `For Cosmo`, `For Cosmo Skyberg` og `For arkitekten
(Cosmo)` konvergerer på `For arkitekten`. To filer kolliderte og er adjudisert
ved å lese dem, ikke ved regel.
Verifisering (alle 7 kriterier fra ordren):
G1 persona på heading-linjer 401 -> 0
G2 døde fragmentlenker 1 -> 1 (pre-eksisterende, unntatt)
G3 produkt-forekomster 451 -> 451; `Cosmos DB|Azure Cosmos` 308 = 308
de 3 kun-produkt-filene byte-identiske
nettet validert begge veier injisert persona feller G1; genitiv feller G1;
produkt-heading og de 3 filene passerer
hele diffen 802 heading-linjer + 654 TOC-linjer, ANNET = 0
linjeantall 728 lagt til = 728 slettet
suite 1120/1120 (1097 + 23 nye)
validate-plugin 250 PASS / 0 FAIL
stikkprøve 10 filer, alle 5 skills, inkl. de 3 mest
produkt-tunge (26/20/19) — kun heading+TOC
Utenfor scope, urørt: de 4 SKILL.md, de 23 commands, CLAUDE.md, README.md,
NOTICE.md, docs/ (alt R14).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 KiB
Streaming Response Patterns
Last updated: 2026-02 Status: GA Category: Performance & Scalability Type: reference
Innhold
- Introduksjon
- Server-Sent Events (SSE) Grunnleggende
- Grunnleggende Streaming-implementasjon
- Chunked Transfer Encoding
- Client-Side Stream Handling
- Error Recovery in Streams
- Nar bruke streaming vs. non-streaming
- Avanserte monstre
- Ytelsesmal for streaming
- For arkitekten
Introduksjon
Streaming av AI-responser er en kritisk teknikk for a forbedre brukeropplevelsen i interaktive AI-applikasjoner. Istedenfor a vente pa at hele responsen genereres for den vises, lar streaming brukeren se svaret bygges opp token for token. For norsk offentlig sektor, der innbyggerportaler og saksbehandlingssystemer i okende grad integrerer AI, er streaming avgjorende for akseptabel responstid.
Azure OpenAI stotter streaming gjennom Server-Sent Events (SSE)-protokollen, som er en enkel, unidireksjonell strommingsmekanisme over HTTP. Denne tilnaermingen er spesielt verdifull for chat-grensesnitt, dokumentgenerering og andre bruksomrader der brukeren forventer umiddelbar tilbakemelding.
Denne referansen dekker arkitekturmonstre for streaming i Azure OpenAI-baserte applikasjoner, fra grunnleggende SSE-implementasjon til avansert feilhandtering og mellomlag-konfigurasjon.
Server-Sent Events (SSE) Grunnleggende
Hva er SSE?
Server-Sent Events er en W3C-standard for enveis stromming fra server til klient over HTTP:
| Egenskap | SSE | WebSocket | Long Polling |
|---|---|---|---|
| Retning | Server -> Klient | Bidireksjonell | Klient -> Server -> Klient |
| Protokoll | HTTP/1.1+ | WebSocket (ws://) | HTTP |
| Automatisk reconnect | Ja (innebygd) | Nei (manuell) | Nei |
| Kompleksitet | Lav | Hoy | Middels |
| Azure OpenAI-stotte | Ja | Ja (Realtime API) | Nei |
SSE-format
Azure OpenAI returnerer data i SSE-format:
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-cache
Connection: keep-alive
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"Hei"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Viktige SSE-regler:
- Hver hendelse er prefixet med
data: - Hendelser separeres med to linjeskift (
\n\n) - Siste hendelse er alltid
data: [DONE] delta-feltet inneholder inkrementelt innhold (ikke kumulativt)finish_reasonernulltil generering er ferdig
Grunnleggende Streaming-implementasjon
Python med Azure OpenAI SDK
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
def stream_chat_response(user_message: str) -> str:
"""Stream en chat completion og bygg opp komplett respons."""
full_response = ""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Du er en hjelpesom assistent."},
{"role": "user", "content": user_message}
],
stream=True,
max_tokens=500
)
for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response += content
print(content, end="", flush=True) # Vis inkrementelt
print() # Ny linje etter ferdig streaming
return full_response
Async Python Streaming
from openai import AsyncAzureOpenAI
import asyncio
async_client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
async def stream_async(user_message: str):
"""Asynkron streaming for hoy-throughput applikasjoner."""
response = await async_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
stream=True,
max_tokens=500
)
collected_content = []
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
collected_content.append(content)
yield content # Yield for videre prosessering
return "".join(collected_content)
TypeScript/JavaScript Streaming
import { AzureOpenAI } from "openai";
const client = new AzureOpenAI({
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key",
apiVersion: "2025-03-01-preview",
});
async function* streamChatResponse(
userMessage: string
): AsyncGenerator<string> {
const stream = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
stream: true,
max_tokens: 500,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
// Bruk i en web-handler
async function handleStreamRequest(req: Request): Promise<Response> {
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
for await (const token of streamChatResponse("Hva er GDPR?")) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: token })}\n\n`));
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(readableStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}
Chunked Transfer Encoding
HTTP-konfigurasjon for streaming
For at streaming skal fungere gjennom hele infrastrukturen, ma alle mellomlag konfigureres korrekt:
| Komponent | Nodvendig konfigurasjon |
|---|---|
| Azure OpenAI | stream: true i request |
| API Management | buffer-response="false" i forward-request |
| Application Gateway | Deaktiver response buffering |
| Azure Front Door | Route-spesifikk konfigurasjon |
| Klient (browser) | Accept: text/event-stream header |
API Management for SSE
<!-- APIM policy for SSE pass-through -->
<policies>
<inbound>
<base />
</inbound>
<backend>
<!-- KRITISK: buffer-response="false" for streaming -->
<forward-request timeout="120"
fail-on-error-status-code="true"
buffer-response="false" />
</backend>
<outbound>
<base />
<!-- VIKTIG: Deaktiver body-logging for SSE-APIer -->
</outbound>
<on-error>
<base />
</on-error>
</policies>
Viktige APIM-hensyn for SSE:
- Deaktiver response buffering (
buffer-response="false") - Deaktiver
validate-content-policy (buffrer respons) - Deaktiver request/response body-logging for Azure Monitor og Application Insights
- Deaktiver response caching for streaming-endepunkter
- Okt timeout (minimum 120 sekunder)
- Hold forbindelser i live med TCP keepalive
Application Gateway for SSE
{
"properties": {
"responseBufferPolicy": {
"responseSendTimeoutInSeconds": 120,
"bufferResponseBody": false
},
"backendHttpSettings": {
"requestTimeout": 120,
"connectionDraining": {
"enabled": true,
"drainTimeoutInSec": 30
}
}
}
}
Azure Front Door Route Policy
For SSE gjennom Azure Front Door:
{
"routePolicy": {
"routeTimeout": "0s"
}
}
Merk: Idle timeout for Application Gateway for Containers er 5 minutter. Send keepalive-meldinger for a forhindre at forbindelsen lukkes:
: keep-alive\n\n
Client-Side Stream Handling
React/Next.js Frontend
// React hook for SSE streaming fra Azure OpenAI
import { useState, useCallback } from "react";
interface StreamState {
content: string;
isStreaming: boolean;
error: string | null;
}
function useAIStream() {
const [state, setState] = useState<StreamState>({
content: "",
isStreaming: false,
error: null,
});
const startStream = useCallback(async (prompt: string) => {
setState({ content: "", isStreaming: true, error: null });
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ message: prompt }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) throw new Error("No reader available");
let accumulated = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
if (parsed.content) {
accumulated += parsed.content;
setState((prev) => ({
...prev,
content: accumulated,
}));
}
} catch {
// Ignorer parsing-feil for ufullstendige chunks
}
}
}
}
setState((prev) => ({ ...prev, isStreaming: false }));
} catch (error) {
setState((prev) => ({
...prev,
isStreaming: false,
error: error instanceof Error ? error.message : "Ukjent feil",
}));
}
}, []);
return { ...state, startStream };
}
Python SSE Client
import httpx
import json
from typing import AsyncGenerator
async def consume_sse_stream(
url: str,
payload: dict,
api_key: str
) -> AsyncGenerator[str, None]:
"""Konsumer SSE-strom fra Azure OpenAI via HTTP."""
headers = {
"Content-Type": "application/json",
"api-key": api_key,
"Accept": "text/event-stream"
}
async with httpx.AsyncClient(timeout=120.0) as client:
async with client.stream("POST", url, json=payload, headers=headers) as response:
response.raise_for_status()
buffer = ""
async for chunk in response.aiter_text():
buffer += chunk
while "\n\n" in buffer:
event, buffer = buffer.split("\n\n", 1)
for line in event.split("\n"):
if line.startswith("data: "):
data = line[6:]
if data == "[DONE]":
return
try:
parsed = json.loads(data)
content = parsed["choices"][0]["delta"].get("content", "")
if content:
yield content
except (json.JSONDecodeError, KeyError, IndexError):
continue
Error Recovery in Streams
Haandtering av avbrutte strommer
Streaming-forbindelser kan avbrytes av flere arsaker:
| Feiltype | Arsak | Handtering |
|---|---|---|
| Nettverksavbrudd | Ustabil forbindelse | Reconnect med checkpoint |
| Timeout | Idle > 4 min (Azure LB) | Keepalive-meldinger |
| 429 Rate Limit | Kapasitetsgrense | Retry med backoff |
| 500 Server Error | Midlertidig serverfeil | Retry etter pause |
| Content Filter | Innhold blokkert | Vis melding til bruker |
Robust Streaming med Retry
import asyncio
import time
from openai import AsyncAzureOpenAI, APIStatusError, APIConnectionError
async_client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
async def resilient_stream(
messages: list,
max_retries: int = 3,
model: str = "gpt-4o"
) -> AsyncGenerator[str, None]:
"""Streaming med automatisk retry og feilhandtering."""
collected_tokens = []
attempt = 0
while attempt < max_retries:
try:
response = await async_client.chat.completions.create(
model=model,
messages=messages,
stream=True,
max_tokens=1000
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
collected_tokens.append(token)
yield token
# Sjekk finish_reason
if chunk.choices and chunk.choices[0].finish_reason:
reason = chunk.choices[0].finish_reason
if reason == "content_filter":
yield "\n[Innhold filtrert av sikkerhetsfilter]"
return # Ferdig
return # Stromming fullfort
except APIStatusError as e:
attempt += 1
if e.status_code == 429:
retry_after = int(e.response.headers.get("retry-after", "5"))
await asyncio.sleep(retry_after)
elif e.status_code >= 500:
await asyncio.sleep(2 ** attempt) # Eksponentiell backoff
else:
raise # Ikke-gjenforsokbar feil
except APIConnectionError:
attempt += 1
await asyncio.sleep(2 ** attempt)
raise Exception(f"Streaming feilet etter {max_retries} forsok")
Streaming med Partial Response Recovery
async def stream_with_checkpoint(
messages: list,
on_token: callable,
on_complete: callable,
on_error: callable
):
"""Stream med checkpoint for delvis gjenoppretting."""
partial_response = []
last_chunk_time = time.time()
try:
response = await async_client.chat.completions.create(
model="gpt-4o",
messages=messages,
stream=True,
max_tokens=1000
)
async for chunk in response:
current_time = time.time()
# Detekter unormalt lang pause mellom chunks
if current_time - last_chunk_time > 30:
# Mulig hengende forbindelse
break
last_chunk_time = current_time
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
partial_response.append(token)
await on_token(token)
if chunk.choices and chunk.choices[0].finish_reason == "stop":
await on_complete("".join(partial_response))
return
# Hvis vi nar hit uten "stop", har streamingen avbrultt
if partial_response:
await on_complete(
"".join(partial_response) +
"\n\n[Merk: Respons kan vaere ufullstendig]"
)
except Exception as e:
if partial_response:
await on_error(e, "".join(partial_response))
else:
await on_error(e, None)
Nar bruke streaming vs. non-streaming
| Scenario | Anbefaling | Begrunnelse |
|---|---|---|
| Chat-grensesnitt | Streaming | Bedre opplevd responstid |
| Innbyggerportal | Streaming | Visuell tilbakemelding under generering |
| Batch-klassifisering | Non-streaming | Kun sluttresultat er relevant |
| Dokumentanalyse | Non-streaming | Strukturert output, ingen inkrementell visning |
| Saksbehandlingsforslag | Streaming | Lang generering, bruker venter |
| API-integrasjon (maskin-til-maskin) | Non-streaming | Enklere feilhandtering |
| Sanntidsoversetning | Streaming | Lavest opplevd latens |
Avanserte monstre
Server-side Streaming Proxy med FastAPI
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncAzureOpenAI
app = FastAPI()
client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest):
"""Server-side proxy for Azure OpenAI streaming."""
async def generate():
try:
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": request.message}],
stream=True,
max_tokens=1000
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
data = {"content": chunk.choices[0].delta.content}
yield f"data: {json.dumps(data)}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
error_data = {"error": str(e)}
yield f"data: {json.dumps(error_data)}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # Deaktiver nginx buffering
}
)
Token-telling under streaming
import tiktoken
async def stream_with_token_counting(messages: list, model: str = "gpt-4o"):
"""Stream med sanntids token-telling for kostnadsovervaking."""
encoding = tiktoken.encoding_for_model(model)
output_tokens = 0
response = await async_client.chat.completions.create(
model=model,
messages=messages,
stream=True,
stream_options={"include_usage": True} # Inkluder bruksdata
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
output_tokens += len(encoding.encode(content))
yield content
# Sjekk usage i siste chunk
if chunk.usage:
actual_tokens = chunk.usage.completion_tokens
cached_tokens = getattr(
chunk.usage.prompt_tokens_details, 'cached_tokens', 0
)
print(f"Faktisk token-bruk: {actual_tokens}")
print(f"Cache-treff: {cached_tokens}")
Ytelsesmal for streaming
| Metrikk | Mal (P95) | Kritisk terskel |
|---|---|---|
| Time to First Token | < 500 ms | > 2000 ms |
| Inter-token latens | < 50 ms | > 200 ms |
| Reconnect-tid | < 2 s | > 10 s |
| Stream completion rate | > 99% | < 95% |
For arkitekten
- Streaming er obligatorisk for alle brukerrettede AI-grensesnitt. Forskjellen i opplevd latens er dramatisk: 200 ms TTFT vs. 3-5 sekunders ventetid for komplett respons.
- Infrastruktur-konfigurasjon er kritisk: Hele kjeden (APIM, App Gateway, Front Door) ma ha response buffering deaktivert. En enkelt feilkonfigurert komponent blokkerer all streaming.
- Feilhandtering i strommer krever eget design: Implementer alltid reconnect-logikk, partial response recovery, og eksponentiell backoff for 429/5xx-feil.
- Content filtering pavirker streaming:
finish_reason: content_filterkan oppsta midt i en strom. Klient-koden ma handtere dette gracefully med en brukermelding. - Token-telling under streaming: Bruk
stream_options: {"include_usage": true}for a fa eksakt token-bruk i siste chunk, viktig for kostnadsovervaking.