fix: avoid OpenAI stream options for Anthropic-compatible providers (#1654)

Integrated into release/v3.7.2
This commit is contained in:
Randi
2026-04-27 06:12:25 -04:00
committed by GitHub
parent 9d334c82b9
commit bc91fb9e54
3 changed files with 76 additions and 11 deletions

View File

@@ -9,7 +9,11 @@ import {
} from "../services/claudeCodeCompatible.ts";
import { getGigachatAccessToken } from "../services/gigachatAuth.ts";
import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
import { getOpenAICompatibleType, isClaudeCodeCompatible } from "../services/provider.ts";
import {
getOpenAICompatibleType,
getTargetFormat,
isClaudeCodeCompatible,
} from "../services/provider.ts";
import { sanitizeQwenThinkingToolChoice } from "../services/qwenThinking.ts";
import { buildDataRobotChatUrl } from "../config/datarobot.ts";
import { buildAzureAiChatUrl } from "../config/azureAi.ts";
@@ -343,19 +347,30 @@ export class DefaultExecutor extends BaseExecutor {
*/
transformRequest(model, body, stream, credentials) {
void model;
void credentials;
const withDefaults = applyProviderRequestDefaults(body, this.config.requestDefaults);
let withDefaults = applyProviderRequestDefaults(body, this.config.requestDefaults);
if (stream && this.config.format === "openai") {
if (typeof withDefaults === "object" && withDefaults !== null) {
withDefaults.stream_options = {
...(withDefaults.stream_options || {}),
include_usage: true,
if (typeof withDefaults === "object" && withDefaults !== null && !Array.isArray(withDefaults)) {
if (this.provider?.startsWith?.("anthropic-compatible-")) {
if (Object.prototype.hasOwnProperty.call(withDefaults, "stream_options")) {
const { stream_options, ...withoutStreamOptions } = withDefaults;
void stream_options;
withDefaults = withoutStreamOptions;
}
} else if (
stream &&
getTargetFormat(this.provider, credentials?.providerSpecificData) === "openai"
) {
withDefaults = {
...withDefaults,
stream_options: {
...(withDefaults.stream_options || {}),
include_usage: true,
},
};
}
}
if (this.provider === "qwen" && typeof body === "object" && body !== null) {
if (this.provider === "qwen" && typeof withDefaults === "object" && withDefaults !== null) {
return sanitizeQwenThinkingToolChoice(withDefaults, "QwenExecutor");
}
return withDefaults;

View File

@@ -557,6 +557,7 @@ test("handleChatCore forces SSE upstream for CC compatible providers while retur
assert.equal(calls.length, 1);
assert.equal(calls[0].headers.Accept, "application/json");
assert.equal(calls[0].body.stream, true);
assert.equal(calls[0].body.stream_options, undefined);
assert.equal(JSON.stringify(calls[0].body).includes('"cache_control"'), false);
const payload = (await result.response.json()) as any;

View File

@@ -582,13 +582,62 @@ test("DefaultExecutor.execute only injects adaptive thinking defaults for Claude
assert.equal((requestBodies[1] as any).output_config, undefined);
});
test("DefaultExecutor.transformRequest is a passthrough and preserves model ids with slashes", () => {
test("DefaultExecutor.transformRequest injects OpenAI stream usage and preserves model ids with slashes", () => {
const executor = new DefaultExecutor("openai");
const body = { model: "zai-org/GLM-5-FP8", messages: [{ role: "user", content: "hi" }] };
const result = executor.transformRequest("zai-org/GLM-5-FP8", body, true, {});
assert.equal(result, body);
assert.notEqual(result, body);
assert.equal(result.model, "zai-org/GLM-5-FP8");
assert.deepEqual((result as any).stream_options, { include_usage: true });
assert.equal((body as any).stream_options, undefined);
});
test("DefaultExecutor.transformRequest only injects stream usage for OpenAI chat targets", () => {
const openAICompat = new DefaultExecutor("openai-compatible-test");
const openAIResponsesCompat = new DefaultExecutor("openai-compatible-responses-test");
const chatBody = { model: "gpt-4.1", messages: [{ role: "user", content: "hi" }] };
const responsesBody = { model: "gpt-4.1", input: "hi" };
const chatResult = openAICompat.transformRequest("gpt-4.1", chatBody, true, {
providerSpecificData: { baseUrl: "https://proxy.example/v1" },
});
const responsesResult = openAIResponsesCompat.transformRequest("gpt-4.1", responsesBody, true, {
providerSpecificData: { baseUrl: "https://proxy.example/v1" },
});
assert.deepEqual((chatResult as any).stream_options, { include_usage: true });
assert.equal((responsesResult as any).stream_options, undefined);
});
test("DefaultExecutor.transformRequest strips stream_options from Anthropic-compatible targets", () => {
const anthropicCompat = new DefaultExecutor("anthropic-compatible-test");
const anthropicCcCompat = new DefaultExecutor("anthropic-compatible-cc-test");
const anthropicBody = {
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream_options: { include_usage: true },
};
const ccBody = {
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
};
const anthropicResult = anthropicCompat.transformRequest(
"claude-sonnet-4-6",
anthropicBody,
true,
{}
);
const ccResult = anthropicCcCompat.transformRequest("claude-sonnet-4-6", ccBody, true, {});
assert.notEqual(anthropicResult, anthropicBody);
assert.equal((anthropicResult as any).stream_options, undefined);
assert.equal((ccResult as any).stream_options, undefined);
});
test("DefaultExecutor.transformRequest neutralizes incompatible tool_choice for Qwen thinking", () => {