From d934fb3efecea92e7be1fe1aa01e58dd381ced3b Mon Sep 17 00:00:00 2001 From: thatssoheil Date: Thu, 6 Aug 2026 06:17:39 -0400 Subject: [PATCH 1/2] fix(stream): inject stream_options for OpenAI-compatible upstreams Streaming requests to OpenAI-compatible providers (e.g. opencode combo -> deepseek-free) recorded IN 0 OUT 0 because the upstream never sent a usage chunk: the client's stream request lacked stream_options.include_usage. Ask for usage in the final chunk (same approach as the iflow executor) so usageHistory and the dashboard reflect real token counts for streams. Closes #3017 --- open-sse/executors/default.js | 8 +++- .../default-executor-stream-usage.test.js | 38 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/unit/default-executor-stream-usage.test.js diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 92c78e92007..a0b58325f8d 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -67,7 +67,7 @@ export class DefaultExecutor extends BaseExecutor { super(provider, PROVIDERS[provider] || PROVIDERS.openai); } - transformRequest(model, body) { + transformRequest(model, body, stream) { const transformed = this.applyJsonSchemaFallback(body); if (transformed && typeof transformed === "object") { @@ -76,6 +76,12 @@ export class DefaultExecutor extends BaseExecutor { delete transformed.client_metadata; } stripUnsupportedParams(this.provider, model, transformed); + // Ask OpenAI-compatible upstreams to include usage in the final stream + // chunk so /v1 streaming requests record real token counts instead of + // IN 0 · OUT 0 (issue #3017). Same approach as the iflow executor. + if (stream && transformed.messages && !transformed.stream_options) { + transformed.stream_options = { include_usage: true }; + } } return injectReasoningContent({ provider: this.provider, model, body: transformed }); diff --git a/tests/unit/default-executor-stream-usage.test.js b/tests/unit/default-executor-stream-usage.test.js new file mode 100644 index 00000000000..24005578360 --- /dev/null +++ b/tests/unit/default-executor-stream-usage.test.js @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { DefaultExecutor } from "../../open-sse/executors/default.js"; + +// Provider registry: opencode is a generic OpenAI-compatible provider → DefaultExecutor. +const executor = new DefaultExecutor("opencode"); + +describe("DefaultExecutor stream_options injection (#3017)", () => { + it("injects stream_options.include_usage for streaming requests", () => { + const body = { + model: "deepseek-v4-flash-free", + messages: [{ role: "user", content: "hi" }], + stream: true, + }; + const out = executor.transformRequest("deepseek-v4-flash-free", body, true); + expect(out.stream_options).toEqual({ include_usage: true }); + }); + + it("does not inject stream_options for non-streaming requests", () => { + const body = { + model: "deepseek-v4-flash-free", + messages: [{ role: "user", content: "hi" }], + stream: false, + }; + const out = executor.transformRequest("deepseek-v4-flash-free", body, false); + expect(out.stream_options).toBeUndefined(); + }); + + it("respects an existing stream_options from the client", () => { + const body = { + model: "deepseek-v4-flash-free", + messages: [{ role: "user", content: "hi" }], + stream: true, + stream_options: { include_usage: false }, + }; + const out = executor.transformRequest("deepseek-v4-flash-free", body, true); + expect(out.stream_options).toEqual({ include_usage: false }); + }); +}); From becd573f9b59f16ba7cb858323ae0f27aef96809 Mon Sep 17 00:00:00 2001 From: Soheil Fakour Date: Wed, 19 Aug 2026 09:03:04 -0400 Subject: [PATCH 2/2] fix(stream): only inject stream_options when body streams Review feedback (#3081): the executor-level stream flag can be true while the translated body omits stream (Responses->chat conversion, Accept: text/event-stream clients). Injecting stream_options into such a body makes strict OpenAI-compatible upstreams (deepseek) 400 with "stream_options should be set along with stream = true". Gate injection on the body's own stream === true, matching what the upstream actually receives. --- open-sse/executors/default.js | 7 ++++++- tests/unit/default-executor-stream-usage.test.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index a0b58325f8d..0f4c2bcca9f 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -79,7 +79,12 @@ export class DefaultExecutor extends BaseExecutor { // Ask OpenAI-compatible upstreams to include usage in the final stream // chunk so /v1 streaming requests record real token counts instead of // IN 0 · OUT 0 (issue #3017). Same approach as the iflow executor. - if (stream && transformed.messages && !transformed.stream_options) { + // Only inject when the body itself streams: the executor-level stream + // flag can be true while the body omits stream (Responses->chat path, + // Accept: text/event-stream clients), and strict upstreams (deepseek) + // 400 with "stream_options should be set along with stream = true". + const bodyStream = transformed.stream === true; + if (stream && bodyStream && transformed.messages && !transformed.stream_options) { transformed.stream_options = { include_usage: true }; } } diff --git a/tests/unit/default-executor-stream-usage.test.js b/tests/unit/default-executor-stream-usage.test.js index 24005578360..ab3fb37ff0d 100644 --- a/tests/unit/default-executor-stream-usage.test.js +++ b/tests/unit/default-executor-stream-usage.test.js @@ -35,4 +35,16 @@ describe("DefaultExecutor stream_options injection (#3017)", () => { const out = executor.transformRequest("deepseek-v4-flash-free", body, true); expect(out.stream_options).toEqual({ include_usage: false }); }); + + it("does not inject when the body omits stream (Responses->chat path)", () => { + // Responses-API clients convert to chat without a stream field while the + // executor-level stream flag is true (Accept: text/event-stream). Strict + // upstreams (deepseek) 400 on stream_options without stream: true. + const body = { + model: "deepseek-v4-flash-free", + messages: [{ role: "user", content: "hi" }], + }; + const out = executor.transformRequest("deepseek-v4-flash-free", body, true); + expect(out.stream_options).toBeUndefined(); + }); });