Repository navigation
CFP (Cloudflare Playground) Streaming Works in Brave Leo AI but Fails in OpenCode with "Stream ended before producing a non-ping SSE event" #12463
Replies: 5 comments 1 reply
✅ Resolved: stream:true → stream:false proxy worksConfirmed root cause matches your analysis — CFP's Playwright + WebSocket startup takes 10-15s, which exceeds the AI SDK's pre-first-chunk window. Solution that worksA small Node proxy that converts #!/usr/bin/env node
// cfp-proxy.mjs - listens on 20130, forwards to OmniRoute 20128
import { createServer } from 'node:http';
const UPSTREAM = 'http://localhost:20128';
const PORT = 20130;
const API_KEY = '<YOUR_OMNIROUTE_KEY>';
const server = createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/v1/chat/completions') {
res.writeHead(404); res.end(); return;
}
const chunks = [];
for await (const c of req) chunks.push(c);
const body = Buffer.concat(chunks).toString();
const parsed = JSON.parse(body);
const wantStream = parsed.stream === true;
const isCfp = parsed.model?.startsWith('cfp/');
// Force non-streaming for CFP, so we wait for the full response
if (isCfp && wantStream) parsed.stream = false;
const upstream = await fetch(`${UPSTREAM}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': req.headers.authorization || `Bearer ${API_KEY}`,
},
body: JSON.stringify(parsed),
});
if (isCfp && wantStream) {
const result = await upstream.json();
if (result.error) {
res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
return;
}
// Re-emit the JSON response as a sequence of SSE chunks
const msg = result.choices?.[0]?.message;
const base = { id: result.id, object: 'chat.completion.chunk',
created: result.created, model: result.model };
res.writeHead(200, { 'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
res.write(`data: ${JSON.stringify({ ...base, choices: [{ index: 0,
delta: { role: 'assistant' }, finish_reason: null }] })}\n\n`);
if (msg?.reasoning_content) {
res.write(`data: ${JSON.stringify({ ...base, choices: [{ index: 0,
delta: { reasoning_content: msg.reasoning_content },
finish_reason: null }] })}\n\n`);
}
if (msg?.content) {
res.write(`data: ${JSON.stringify({ ...base, choices: [{ index: 0,
delta: { content: msg.content }, finish_reason: null }] })}\n\n`);
}
res.write(`data: ${JSON.stringify({ ...base, choices: [{ index: 0,
delta: {}, finish_reason: 'stop' }], usage: result.usage })}\n\n`);
res.write('data: [DONE]\n\n');
res.end();
return;
}
// Non-CFP or already non-streaming: pass through
res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
res.end(await upstream.text());
});
server.listen(PORT, '127.0.0.1');OpenCode config Non-CFP models keep using the original omniroute provider with native streaming. Only omniroute-cfp routes through the proxy. Trade-off The user sees a 10-15s blank window before any chunks arrive (because we wait for the full response). Once it arrives, reasoning_content and content stream back-to-back. This is the price of bypassing the timeout — there's no way to have both sub-second TTFB and survive the CFP startup. Other things I tried that didn't work
Suggestion for OmniRoute itself Two upstream changes would eliminate the need for an external proxy:
|
|
Glad you got it working, @linuxtopia — and thanks for coming back to post the actual solution instead of leaving it hanging. Your diagnosis holds up: the failure is in the window before the first chunk, not in the stream itself, which is why Your proxy works because non-streaming has no pre-first-chunk window to exceed — nothing is sent until everything is ready. That is the correct workaround for the constraint, not a hack. Worth noting for anyone who lands here: this is specific to providers with an expensive session bring-up. CFP has to start Playwright and establish a WebSocket before the first token can exist, so the gap between "request accepted" and "first byte" is structural, not load-dependent. Providers that talk straight to an HTTP API do not have this shape. The real fix on our side would be emitting a keep-alive during that bring-up so the client's timer never fires — the same trick your proxy sidesteps by not streaming at all. Want me to open an issue for that? Your write-up is already a complete repro, and I would credit it. Say the word and I will file it, or file it yourself if you would rather have it under your name. |
|
@linuxtopia — fair ask, and yes, I would rather fix this in OmniRoute than have every user rediscover your proxy. Being precise about what the fix is, since "make it work" has more than one implementation and they are not equally good: Not switching CFP to non-streaming. Your proxy is the correct workaround for you, but as a built-in it would take streaming away from clients that handle the delay fine — trading a real capability for a compatibility patch. The right shape is to make the pre-first-chunk window survivable. Your own diagnosis contains the fix: the failure is entirely in the gap between "request accepted" and "first token", and the AI SDK gives up in that gap. If OmniRoute opens the SSE stream and emits a keep-alive preamble as soon as the request is accepted — before the upstream has produced anything — the client's pre-first-chunk timer never fires, because from its side bytes are arriving. The Playwright/WebSocket bring-up then completes underneath, and the real first chunk follows. That is a small change and it fixes the whole class, not just CFP: any provider with an expensive session bring-up has the same structural gap, as I noted above. Two things it has to get right, which is why it is not a one-liner:
I am not putting a date on it. Keep your proxy running in the meantime — it is not a hack, it is the correct answer for the constraint you have today. And thanks again for posting the actual diagnosis rather than just "it does not work". The reason this has a clear fix is that you did the hard half. |
|
@linuxtopia — I owe you a correction on my last reply here, and it changes the fix. That error string is ours, not the client's. So my 09-07 answer was wrong in its premise. I described it as the AI SDK giving up during the pre-first-chunk gap and proposed a keepalive preamble. A keepalive cannot rescue a stream the upstream already closed, so that would not have fixed your case. Two related facts, since they are easy to conflate:
What actually distinguishes your two runs is the request, not the transport. curl and Brave Leo send a plain chat payload; OpenCode sends tools, a large system prompt and its own parameter set to the same endpoint. A CFP session that ends the stream with nothing for one payload and streams fine for another points at the upstream playground rejecting or dropping that request shape. To pin it down I need two things:
Sorry for the detour — you did the careful half of this and deserved a correct reading of it. |
|
@linuxtopia -- still happening on the latest version (3.8.52)? Otherwise I'll close this in 7 days for inactivity. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Environment
@ai-sdk/openai-compatible)cfp/deepseek-ai/deepseek-v4-pro-0813,cfp/zai-org/glm-5.2)~/.cache/ms-playwright/)Problem Description
CFP models work perfectly via:
But fail consistently in OpenCode with:
The stream never produces any content chunks in OpenCode, even though OmniRoute logs show successful responses.
Reproduction Steps
1. curl Test (✅ Works)
Non-streaming:
Response: ✅ Full response with content
Streaming:
Response: ✅ 60+ SSE chunks with
data: {...}format, includes reasoning_content and content, ends withdata: [DONE]2. Via Brave's Leo Bring Your Own Model (BYOM) AI Test for Omniroute-CFP (✅ Works)
Configuration in Brave BYOM:
Result: ✅ Full 4-paragraph response, streaming works perfectly
3. OpenCode Test (❌ Fails)
OpenCode Config (
~/.config/opencode/opencode.json):{ "provider": { "omniroute": { "npm": "@ai-sdk/openai-compatible", "name": "OmniRoute", "options": { "baseURL": "http://localhost:20128/v1", "apiKey": "sk-c..." }, "models": { "cfp/deepseek-ai/deepseek-v4-pro-0813": { "name": "CFP DeepSeek V4 Pro", "maxTokens": 8192 }, "cfp/zai-org/glm-5.2": { "name": "CFP GLM 5.2", "maxTokens": 8192 } } } } }Test in OpenCode:
Stream ended before producing a non-ping SSE event [retrying attempt #2]Analysis
Observations
@ai-sdk/openai-compatiblefails with CFP models specificallykiro/claude-sonnet-4.5,puter/gpt-4o)Hypothesis: First-Chunk Timeout
Based on research into Vercel AI SDK issues (vercel/ai#16538), the
@ai-sdk/openai-compatiblelibrary has streaming timeout issues:cf_agentRPC frames to SSEchunkMstimeout doesn't cover this "pre-first-chunk window"Timing Breakdown
Why Brave Leo AI Works But OpenCode Doesn't
Brave Leo AI:
OpenCode:
@ai-sdk/openai-compatibleAttempted Solutions
1. ✅ Installed Playwright
Confirmed working: OmniRoute can launch headless Chromium and CFP models work via curl.
2. ❌ Added API Key to OpenCode Config
No change in behavior.
3. ❌ Increased
maxTokensNo change in behavior.
4. ❌ Built Custom Proxy
Created a Node.js proxy to convert streaming to non-streaming, then wrap back to SSE:
Result: Proxy worked with curl, but OpenCode still timed out.
Questions
Is there a way to configure streaming timeouts in OpenCode?
@ai-sdk/openai-compatiblebe configured to wait longer for the first chunk?Is this a known limitation of CFP models with OpenCode?
Can OmniRoute detect slow-starting providers and add SSE ping frames?
data: {"type":"ping"}frames during the Playwright startup phase?Request
Is there a way to make CFP models work in OpenCode, or is this a fundamental incompatibility between:
Any guidance would be appreciated!
Additional Context:
All reactions