mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
fix(executors): strip stream_options for qwen non-streaming / thinking Claude Code requests (port from 9router#663) (#4374)
Claude-Code-compatible providers force the executor-level `stream` flag on
via `upstreamStream = stream || isClaudeCodeCompatible`
(open-sse/handlers/chatCore.ts), but the outgoing body keeps the caller's
original `stream: false`. The shared `stream && targetFormat === "openai"`
branch in DefaultExecutor.transformRequest then injected
`stream_options: { include_usage: true }` onto a body that still said
`stream: false`, and qwen upstream rejected the request with
`400 "'stream_options' only set this when you set stream: true"`. The same
rejection surfaced when the body carried `thinking` / `enable_thinking`.
The qwen branch now skips the injection (and strips any client-sent
`stream_options`) when the body explicitly says `stream: false` or
requests thinking, leaving regular qwen streaming requests with the
include_usage injection intact. Other providers are unaffected.
Adds a TDD regression with 4 cases covering both opt-out paths and the
normal-streaming positive control.
Inspired-by: https://github.com/decolua/9router/pull/663
Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>
This commit is contained in:
committed by
GitHub
parent
b41738197d
commit
3c3dcadd4c
@@ -121,6 +121,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
|
||||
- **fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security)** — addresses the undici SOCKS5-TLS / cache advisories and the dompurify advisory. ([#4306](https://github.com/diegosouzapw/OmniRoute/pull/4306))
|
||||
- **chore(deps): bump actions/checkout from 4 to 7** — CI checkout-action update. ([#4297](https://github.com/diegosouzapw/OmniRoute/pull/4297))
|
||||
- **fix(executors): strip `stream_options` for qwen non-streaming / thinking-mode Claude Code requests** — Claude-Code-compatible providers force the executor-level `stream` flag on via `upstreamStream = stream || isClaudeCodeCompatible` (`open-sse/handlers/chatCore.ts`), but the outgoing body keeps the caller's original `stream: false`. The shared `stream && targetFormat === "openai"` branch in `DefaultExecutor.transformRequest` then injected `stream_options: { include_usage: true }` onto a body that still said `stream: false`, and qwen upstream rejected it with `400 "'stream_options' only set this when you set stream: true"`. Same rejection when the body carries `thinking` / `enable_thinking`. The qwen branch now skips the injection (and strips any client-sent `stream_options`) when the body explicitly says `stream: false` or requests thinking, leaving regular qwen streaming requests with the usage injection intact. (thanks @anuragg-saxenaa)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -571,11 +571,31 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
withDefaults = withoutStreamOptions;
|
||||
}
|
||||
} else if (stream && targetFormat === "openai" && requestFormat !== "openai-responses") {
|
||||
if (!credentials?.providerSpecificData?.disableStreamOptions) {
|
||||
// Port of decolua/9router#663 (closes upstream #557): Qwen rejects with
|
||||
// 400 "'stream_options' only set this when you set stream: true" when the
|
||||
// outgoing body carries `stream: false` (Claude Code / Claude-Code-
|
||||
// compatible callers force the executor-level stream flag on via
|
||||
// `upstreamStream = stream || isClaudeCodeCompatible`, but the body keeps
|
||||
// the caller's original `stream: false`). Same upstream also rejects the
|
||||
// injection when `thinking` / `enable_thinking` is set. Skip injection in
|
||||
// those cases instead of unconditionally adding `stream_options`.
|
||||
const defaultsRecord = withDefaults as Record<string, unknown>;
|
||||
const qwenBlocksStreamOptions =
|
||||
this.provider === "qwen" &&
|
||||
(defaultsRecord.stream === false ||
|
||||
Boolean(defaultsRecord.thinking) ||
|
||||
Boolean(defaultsRecord.enable_thinking));
|
||||
if (qwenBlocksStreamOptions) {
|
||||
if (Object.prototype.hasOwnProperty.call(defaultsRecord, "stream_options")) {
|
||||
const withoutStreamOptions = { ...defaultsRecord };
|
||||
delete withoutStreamOptions.stream_options;
|
||||
withDefaults = withoutStreamOptions;
|
||||
}
|
||||
} else if (!credentials?.providerSpecificData?.disableStreamOptions) {
|
||||
withDefaults = {
|
||||
...withDefaults,
|
||||
stream_options: {
|
||||
...(((withDefaults as Record<string, unknown>).stream_options as object) || {}),
|
||||
...((defaultsRecord.stream_options as object) || {}),
|
||||
include_usage: true,
|
||||
},
|
||||
};
|
||||
|
||||
100
tests/unit/qwen-strip-stream-options-claude-code-port663.test.ts
Normal file
100
tests/unit/qwen-strip-stream-options-claude-code-port663.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Port of upstream decolua/9router#663 (closes upstream #557).
|
||||
*
|
||||
* Scenario: Claude Code (or any caller) hits a Qwen model with an OpenAI body
|
||||
* that carries `stream: false`. OmniRoute, however, sets the executor-level
|
||||
* `stream` flag to `true` for Claude-Code-compatible providers via
|
||||
* `upstreamStream = stream || isClaudeCodeCompatible`
|
||||
* (`open-sse/handlers/chatCore.ts`). DefaultExecutor.transformRequest then runs
|
||||
* its `if (stream && targetFormat === "openai")` branch and injects
|
||||
* `stream_options: { include_usage: true }` onto a body that still carries
|
||||
* `stream: false`. Qwen upstream rejects with:
|
||||
* 400 "'stream_options' only set this when you set stream: true"
|
||||
*
|
||||
* Fix mirrors upstream: when the OUTGOING body explicitly says `stream: false`,
|
||||
* do NOT inject `stream_options` regardless of the executor-level `stream` arg.
|
||||
* Same defensive treatment when the body carries `thinking` /
|
||||
* `enable_thinking`, since the upstream PR also exempts those.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||||
|
||||
test("port#663 qwen: body.stream===false → no stream_options even when executor stream=true", () => {
|
||||
const executor = new DefaultExecutor("qwen");
|
||||
const body = {
|
||||
model: "qwen3-coder-plus",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
stream: false,
|
||||
};
|
||||
const result = executor.transformRequest(
|
||||
"qwen3-coder-plus",
|
||||
body,
|
||||
/* stream */ true,
|
||||
{}
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(
|
||||
result.stream_options,
|
||||
undefined,
|
||||
"stream_options must not be injected when body.stream === false"
|
||||
);
|
||||
});
|
||||
|
||||
test("port#663 qwen: body.thinking truthy → no stream_options injection", () => {
|
||||
const executor = new DefaultExecutor("qwen");
|
||||
const body = {
|
||||
model: "qwen3-coder-plus",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
thinking: { type: "enabled" },
|
||||
};
|
||||
const result = executor.transformRequest(
|
||||
"qwen3-coder-plus",
|
||||
body,
|
||||
true,
|
||||
{}
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(
|
||||
result.stream_options,
|
||||
undefined,
|
||||
"stream_options must not be injected when thinking mode is requested"
|
||||
);
|
||||
});
|
||||
|
||||
test("port#663 qwen: body.enable_thinking truthy → no stream_options injection", () => {
|
||||
const executor = new DefaultExecutor("qwen");
|
||||
const body = {
|
||||
model: "qwen3-coder-plus",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
enable_thinking: true,
|
||||
};
|
||||
const result = executor.transformRequest(
|
||||
"qwen3-coder-plus",
|
||||
body,
|
||||
true,
|
||||
{}
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(
|
||||
result.stream_options,
|
||||
undefined,
|
||||
"stream_options must not be injected when enable_thinking is true"
|
||||
);
|
||||
});
|
||||
|
||||
test("port#663 qwen: normal streaming request still injects stream_options.include_usage", () => {
|
||||
const executor = new DefaultExecutor("qwen");
|
||||
const body = {
|
||||
model: "qwen3-coder-plus",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = executor.transformRequest(
|
||||
"qwen3-coder-plus",
|
||||
body,
|
||||
true,
|
||||
{}
|
||||
) as Record<string, unknown>;
|
||||
assert.deepEqual(
|
||||
result.stream_options,
|
||||
{ include_usage: true },
|
||||
"regular qwen streaming requests must keep the include_usage injection"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user