fix(translator): preserve reasoning_effort for non-Copilot Responses clients (#4688)

Integrated into release/v3.8.37 — preserve reasoning_effort for non-Copilot Responses clients. Cherry-picked onto release tip; tests 47/47 green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 21:27:19 -03:00
committed by GitHub
parent a2ce88c8eb
commit fd8a52ed64
3 changed files with 88 additions and 7 deletions

View File

@@ -445,11 +445,14 @@ export function openaiResponsesToOpenAIRequest(
}
delete result.store;
// Copilot-only: promote Responses `reasoning.{effort,summary}` to Chat fields
// so the downstream openai-to-claude translator can enable extended thinking.
// Gated by the UA marker from translateRequest; other clients see `reasoning` dropped.
// Promote Responses `reasoning.effort` to the Chat-Completions-native
// `reasoning_effort` field so OpenAI-family upstreams (and the downstream
// openai-to-claude translator's extended-thinking path) keep the hint when a
// Responses client is routed across formats. The Copilot-only `summary` ->
// Claude summarized-thinking marker stays behind the UA gate from
// translateRequest because it is Copilot-specific glue, not an OpenAI-native
// field. Ported from upstream PR decolua/9router#1817 (ryanngit).
if (
credentialRecord._copilotClient === true &&
root.reasoning &&
typeof root.reasoning === "object" &&
!Array.isArray(root.reasoning)
@@ -459,7 +462,10 @@ export function openaiResponsesToOpenAIRequest(
if (effort && result.reasoning_effort === undefined) {
result.reasoning_effort = normalizeResponsesReasoningEffort(effort);
}
if (shouldRequestClaudeSummarizedThinking(reasoningRec.summary)) {
if (
credentialRecord._copilotClient === true &&
shouldRequestClaudeSummarizedThinking(reasoningRec.summary)
) {
result[COPILOT_REASONING_SUMMARY_MARKER] = "summarized";
}
}

View File

@@ -0,0 +1,72 @@
/**
* Reasoning-effort mapping across the OpenAI Chat <-> Responses request translators.
*
* Chat Completions carries the reasoning hint as top-level `reasoning_effort`; the
* Responses API nests it as `reasoning.effort`. These tests pin both directions so
* the hint survives when a request crosses formats (e.g. a Responses client routed
* to an OpenAI-native Chat Completions upstream).
*
* Ported from upstream PR https://github.com/decolua/9router/pull/1817 (ryanngit).
* Adapted: OmniRoute previously promoted `reasoning.effort` only behind the
* Copilot-client gate (commit 75d9a83c25), which silently dropped the field for
* every other Responses client (OpenCode, Cursor, raw OpenAI Responses, ...).
* This test pins the unconditional promotion of effort while keeping the
* Copilot-only `summary` -> Claude thinking marker behind its existing gate.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
openaiToOpenAIResponsesRequest,
openaiResponsesToOpenAIRequest,
} from "../../open-sse/translator/request/openai-responses.ts";
import { convertResponsesApiFormat } from "../../open-sse/translator/helpers/responsesApiHelper.ts";
function asRecord(value: unknown): Record<string, unknown> {
return value as Record<string, unknown>;
}
test("Responses -> Chat promotes reasoning.effort for non-Copilot clients", () => {
const out = asRecord(
openaiResponsesToOpenAIRequest(
"gpt-test",
{ input: "hello", reasoning: { effort: "high" } },
true,
{} // no _copilotClient marker
)
);
assert.equal(out.reasoning_effort, "high");
assert.equal(out.reasoning, undefined);
});
test("Responses -> Chat preserves reasoning.effort via the helper wrapper", () => {
const out = asRecord(
convertResponsesApiFormat({ input: "hello", reasoning: { effort: "medium" } })
);
assert.equal(out.reasoning_effort, "medium");
assert.equal(out.reasoning, undefined);
});
test("Responses -> Chat does not overwrite an explicit reasoning_effort", () => {
const out = asRecord(
openaiResponsesToOpenAIRequest(
"gpt-test",
{ input: "hello", reasoning_effort: "low", reasoning: { effort: "high" } },
true,
{}
)
);
// Explicit Chat-level field wins over the Responses nesting.
assert.equal(out.reasoning_effort, "low");
});
test("Chat -> Responses already wraps reasoning_effort into reasoning.effort", () => {
const out = asRecord(
openaiToOpenAIResponsesRequest(
"gpt-test",
{ messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
true,
{}
)
);
assert.deepEqual(out.reasoning, { effort: "high" });
});

View File

@@ -623,7 +623,10 @@ test("Chat -> Responses prefers max_completion_tokens over max_tokens when both
assert.equal((result as any).max_completion_tokens, undefined);
});
test("Responses -> Chat drops `reasoning` and does not synthesize reasoning_effort without Copilot marker", () => {
test("Responses -> Chat drops `reasoning` and promotes effort to reasoning_effort even without Copilot marker", () => {
// Updated per upstream PR decolua/9router#1817 (ryanngit): the OpenAI-native
// `reasoning_effort` hint is always preserved across the Responses -> Chat
// hop; only the Copilot-specific `summary` -> Claude marker stays gated.
const result = openaiResponsesToOpenAIRequest(
"claude-opus-4-7",
{
@@ -635,7 +638,7 @@ test("Responses -> Chat drops `reasoning` and does not synthesize reasoning_effo
) as Record<string, unknown>;
assert.equal(result.reasoning, undefined);
assert.equal(result.reasoning_effort, undefined);
assert.equal(result.reasoning_effort, "high");
});
test("Responses -> Chat promotes reasoning.effort to reasoning_effort when _copilotClient is set", () => {