mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
fix(sse): cline forceStream must stream upstream only, keep client JSON
The #2081 wiring fed providerRequiresStreaming into resolveStreamFlag, forcing the client-facing stream flag to true for forceStream providers. That skips the if(!stream) branch that drains a forced upstream SSE and converts it back to JSON, so a stream:false caller (model-test button, plain JSON API) got STREAM_EARLY_EOF instead of a JSON body. Keep providerRequiresStreaming only on upstreamStream (force upstream to stream); leave the client-facing stream as the client sent it, so readNonStreamingResponseBody accumulates the SSE into JSON. The promised handleForcedSSEToJson (#2081 comment) was never implemented — this uses the existing non-streaming SSE-buffering path (same as isClaudeCodeCompatible). Live-verified on VPS: cline stream:true worked, stream:false failed.
This commit is contained in:
@@ -873,9 +873,16 @@ export async function handleChatCore({
|
||||
// sourceFormat="claude" applies the Anthropic Messages spec default (stream=false
|
||||
// when body omits stream), preventing STREAM_EARLY_EOF on /v1/messages when
|
||||
// clients send Accept: */* without an explicit stream flag.
|
||||
// providerRequiresStreaming: providers with forceStream:true reject stream:false
|
||||
// upstream (HTTP 400); keep streaming so OmniRoute can convert the stream to JSON
|
||||
// for the client via handleForcedSSEToJson. (#2081)
|
||||
// providerRequiresStreaming: providers with forceStream:true (cline/clinepass)
|
||||
// only implement upstream streaming — a non-streaming request returns
|
||||
// "generateText is not implemented" / an empty body. This flag forces the
|
||||
// UPSTREAM request to stream (see `upstreamStream` below), but it MUST NOT
|
||||
// force the client-facing `stream` flag: a stream:false client (e.g. the
|
||||
// model-test button, plain JSON API callers) still expects a JSON response.
|
||||
// The client-side `if (!stream)` branch drains the forced upstream SSE and
|
||||
// converts it back to JSON via readNonStreamingResponseBody. Passing this
|
||||
// flag into resolveStreamFlag would force `stream=true` and skip that
|
||||
// conversion, yielding STREAM_EARLY_EOF for JSON callers. (#2081, #6126)
|
||||
const providerRequiresStreaming = REGISTRY[provider]?.forceStream === true;
|
||||
const stream =
|
||||
nativeCodexPassthrough && isCompactResponsesEndpoint(endpointPath)
|
||||
@@ -883,7 +890,6 @@ export async function handleChatCore({
|
||||
: resolveStreamFlag(body?.stream, acceptHeader, sourceFormat, {
|
||||
userAgent: streamUserAgent,
|
||||
streamDefaultMode: apiKeyInfo?.streamDefaultMode,
|
||||
providerRequiresStreaming,
|
||||
});
|
||||
|
||||
// `settings` is already consolidated once near the top of handleChatCore
|
||||
|
||||
@@ -5,11 +5,14 @@ import { REGISTRY } from "@omniroute/open-sse/config/providers/index.ts";
|
||||
import { resolveStreamFlag } from "@omniroute/open-sse/utils/aiSdkCompat.ts";
|
||||
|
||||
// Cline / ClinePass only implement upstream streaming — a non-streaming request
|
||||
// returns "generateText is not implemented" / an empty body. They must carry
|
||||
// `forceStream: true` so chatCore forces upstream streaming (upstreamStream) even
|
||||
// when the client wants JSON, then converts the SSE back to JSON. Regression guard
|
||||
// for the "cline model test → generateText is not implemented / empty response"
|
||||
// bug (live-verified on the VPS: stream:true works, stream:false failed).
|
||||
// returns "generateText is not implemented" / an empty body. They carry
|
||||
// `forceStream: true` so chatCore forces the UPSTREAM request to stream
|
||||
// (`upstreamStream = stream || isClaudeCodeCompatible || providerRequiresStreaming`)
|
||||
// even when the client wants JSON. The client-facing `stream` flag stays as the
|
||||
// client sent it, so the `if (!stream)` branch drains the forced upstream SSE and
|
||||
// converts it back to JSON via readNonStreamingResponseBody. Regression guard for
|
||||
// the "cline model test → generateText is not implemented / STREAM_EARLY_EOF" bug
|
||||
// (live-verified on the VPS: stream:true works, stream:false failed). (#6126)
|
||||
|
||||
test("cline provider is flagged forceStream (streaming-only upstream)", () => {
|
||||
assert.equal(REGISTRY.cline?.forceStream, true);
|
||||
@@ -19,19 +22,22 @@ test("clinepass provider is flagged forceStream (streaming-only upstream)", () =
|
||||
assert.equal(REGISTRY.clinepass?.forceStream, true);
|
||||
});
|
||||
|
||||
test("resolveStreamFlag forces streaming for a forceStream provider even when the client sent stream:false", () => {
|
||||
// providerRequiresStreaming derives from REGISTRY[provider].forceStream === true
|
||||
test("upstreamStream is forced true for a forceStream provider even when the client sent stream:false", () => {
|
||||
// Mirror the chatCore wiring: providerRequiresStreaming derives from the
|
||||
// registry flag, and upstreamStream ORs it in so the upstream always streams.
|
||||
const providerRequiresStreaming = REGISTRY.cline?.forceStream === true;
|
||||
assert.equal(
|
||||
resolveStreamFlag(false, "application/json", "openai", { providerRequiresStreaming }),
|
||||
true
|
||||
);
|
||||
const isClaudeCodeCompatible = false;
|
||||
const clientStream = false; // client asked for JSON
|
||||
const upstreamStream = clientStream || isClaudeCodeCompatible || providerRequiresStreaming;
|
||||
assert.equal(upstreamStream, true);
|
||||
});
|
||||
|
||||
test("resolveStreamFlag still honors stream:false for a normal (non-forceStream) provider", () => {
|
||||
const providerRequiresStreaming = REGISTRY.openai?.forceStream === true; // false
|
||||
assert.equal(
|
||||
resolveStreamFlag(false, "application/json", "openai", { providerRequiresStreaming }),
|
||||
false
|
||||
);
|
||||
test("client-facing stream stays false for a stream:false JSON caller (so SSE→JSON conversion runs)", () => {
|
||||
// chatCore MUST NOT pass providerRequiresStreaming into resolveStreamFlag:
|
||||
// a stream:false client keeps stream=false so the `if (!stream)` branch drains
|
||||
// the forced upstream SSE and returns JSON. Forcing stream=true here would skip
|
||||
// that conversion and yield STREAM_EARLY_EOF for JSON callers.
|
||||
assert.equal(resolveStreamFlag(false, "application/json", "openai"), false);
|
||||
// A stream:true client still streams end-to-end.
|
||||
assert.equal(resolveStreamFlag(true, "application/json", "openai"), true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user