feat: streaming (SSE) end-to-end

- Provider::chat_stream; OpenAI provider streams via stream_options.include_usage
- pipeline.run_stream: shared route/select, failover before first byte
- /v1/chat/completions forwards SSE verbatim with X-Cortiq-* headers; usage tapped for stats
- admin /test/stream + live streaming in Playground (stream toggle, 7 langs)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
infosave2007
2026-06-30 16:37:35 +03:00
co-authored by Claude Opus 4.8
parent 220df29888
commit b6fdf23aa6
17 changed files with 483 additions and 64 deletions
+31
View File
@@ -154,4 +154,35 @@ impl Provider for OpenAiProvider {
},
})
}
async fn chat_stream(&self, req: ChatRequest) -> Result<super::ChatStream> {
use futures::StreamExt;
let mut body = self.to_wire(&req);
body["stream"] = serde_json::Value::Bool(true);
// ask OpenAI-compatible servers to include token usage in the final chunk
body["stream_options"] = serde_json::json!({ "include_usage": true });
let mut r = self
.http
.post(format!("{}/chat/completions", self.base_url))
.json(&body);
if let Some(key) = &self.api_key {
r = r.bearer_auth(key);
}
let resp = r
.send()
.await
.map_err(|e| GatewayError::UpstreamUnavailable(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status();
let err_body = resp.text().await.unwrap_or_default();
return Err(GatewayError::UpstreamUnavailable(format!(
"provider {} returned HTTP {}: {}",
self.id, status, err_body
)));
}
let stream = resp
.bytes_stream()
.map(|res| res.map_err(|e| GatewayError::UpstreamUnavailable(e.to_string())));
Ok(Box::pin(stream))
}
}