Se completó FASE 29.2: eliminación del pseudo-SSE introducido en FASE 27.1.1 y reactivación del streaming real end-to-end.
Problema
Section titled “Problema”Desde FASE 27.1.1, el gateway hardcodeaba stream=False contra LM Studio. Esperaba la response completa, la bufferizaba en memoria con response.json(), y luego sintetizaba 2 chunks SSE artificiales + [DONE]. El resultado era pseudo-streaming: el cliente veía un spinner hasta recibir todo el texto de golpe.
- TTFB = completion completa (~10-13s)
stream_chunks_total= siempre 2 por requestcompletion_stream_duration≈ 0ms (medición tautológica)- Sin medición real de primer token
Solución
Section titled “Solución”Se reactivó relay_stream() — un módulo de 17 líneas que existía en versiones legacy del gateway pero fue reemplazado durante FASE 27.1.1:
def relay_stream(upstream, handler, model="unknown"): for chunk in upstream.iter_content(chunk_size=8192): if not chunk: continue handler.wfile.write(chunk) handler.wfile.flush()El gateway ahora:
- Envía
stream=truea LM Studio cuando el cliente lo solicita - Usa
requests.post(stream=True)para no bufferizar - Relaya cada chunk directamente al cliente sin esperar completion
Feature flag
Section titled “Feature flag”AI_LAB_REAL_STREAMING=true # streaming realAI_LAB_REAL_STREAMING=false # fallback fake SSE (legacy)Rollback instantáneo sin reiniciar.
Backpressure
Section titled “Backpressure”MAX_CONCURRENT_STREAMS = 3MAX_STREAM_DURATION_SEC = 300FIRST_CHUNK_TIMEOUT_SEC = 20Con finally{} que limpia slots, cierra upstream y libera recursos.
Resultados
Section titled “Resultados”| Métrica | Antes (fake SSE) | Ahora (real) |
|---|---|---|
| Chunks por request | 2 (sintéticos) | 79 (reales) |
| TTFB | ~10-13s (respuesta completa) | ~1.5s (primer chunk) |
| curl -N | Spinner → todo de golpe | Texto progresivo |
| Streams activos | 0 (siempre) | Variable real |
| Gateway | Estable | Estable |
Arquitectura de timeouts (4 capas)
Section titled “Arquitectura de timeouts (4 capas)”connect_timeout = 5s TCP handshake con LM Studiofirst_chunk_timeout = 20s TTFB deadline (primer token)stream_idle_timeout = 30s Máx inactividad entre chunkscompletion_timeout = 300s Tiempo total máximoArchivos modificados
Section titled “Archivos modificados”runtime/gateway/stream_sanitizer.py— 17 → 112 líneas (backpressure, timeouts, cleanup, métricas)runtime/gateway/openai_gateway.py—stream=False→stream=stream_enabledcondicional, relay real con fallback legacyruntime/telemetry/prometheus_metrics.py— métricasailab_stream_first_chunk_ms,ailab_stream_chunks_totalya activas