fix(providers): read reasoning_text in Claude-format response translator (#7856) (#7919)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-20 22:22:49 -03:00
committed by GitHub
parent b15f343e67
commit 583d3ebe1d
3 changed files with 53 additions and 19 deletions

View File

@@ -0,0 +1 @@
- fix(providers): Copilot reasoning_text now surfaces in Claude-format non-stream responses, fixing spurious 502s (#7856)

View File

@@ -5,6 +5,7 @@ import {
} from "../services/geminiThoughtSignatureStore.ts";
import { normalizeOpenAICompatibleFinishReasonString } from "../utils/finishReason.ts";
import { containsTextualToolCallMarker } from "../utils/textualToolCall.ts";
import { getAnyReasoningValue } from "../utils/reasoningFields.ts";
type JsonRecord = Record<string, unknown>;
@@ -576,27 +577,15 @@ export function translateNonStreamingResponse(
/**
* Resolve reasoning/thinking text off a non-streaming OpenAI-format message object.
* Checks DeepSeek-style `reasoning_content`, then the OpenRouter/StepFun aliases
* `reasoning` and `reasoning_details[]` (array of { text | content }), mirroring the
* streaming translator's fallback chain in open-sse/translator/response/openai-to-claude.ts.
* Delegates to the shared reasoning-field resolver (`open-sse/utils/reasoningFields.ts`)
* so every reasoning alias — DeepSeek-style `reasoning_content`, the OpenRouter/StepFun
* `reasoning` string, GitHub Copilot's `reasoning_text`, `thinking`/`thought`, and
* `reasoning_details[]` (array of { text | content }) — is read from one place instead
* of a divergent local copy. Mirrors the streaming translator's fallback chain in
* open-sse/translator/response/openai-to-claude.ts.
*/
function resolveReasoningText(messageObj: JsonRecord): string {
if (messageObj.reasoning_content) {
return toString(messageObj.reasoning_content);
}
if (typeof messageObj.reasoning === "string" && messageObj.reasoning) {
return messageObj.reasoning;
}
if (Array.isArray(messageObj.reasoning_details)) {
const parts: string[] = [];
for (const detail of messageObj.reasoning_details) {
const detailObj = toRecord(detail);
const text = detailObj.text ?? detailObj.content;
if (typeof text === "string" && text) parts.push(text);
}
return parts.join("");
}
return "";
return getAnyReasoningValue(messageObj);
}
/**

View File

@@ -0,0 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts";
import { detectMalformedNonStream } from "../../open-sse/utils/diagnostics.ts";
// GitHub Copilot (github/gemini-3.x models) returns reasoning in `message.reasoning_text`,
// with `content` left empty. Before the fix, resolveReasoningText() in responseTranslator.ts
// only checked reasoning_content / reasoning / reasoning_details[] and missed reasoning_text,
// so the Claude-format translation dropped the reasoning entirely and emitted a bare
// "(empty response)" text block, which detectMalformedNonStream() then rejects (-> 502).
const copilotStyleResponse = {
id: "chatcmpl-copilot-7856",
object: "chat.completion",
created: 1783700000,
model: "github/gemini-3-pro",
choices: [
{
index: 0,
finish_reason: "stop",
message: {
role: "assistant",
content: "",
reasoning_text: "Let me think about the user's request before answering.",
},
},
],
usage: { prompt_tokens: 12, completion_tokens: 8, total_tokens: 20 },
};
test("#7856 /v1/messages non-stream translation surfaces Copilot reasoning_text as a thinking block", () => {
const translated = translateNonStreamingResponse(copilotStyleResponse, "openai", "claude", null) as {
content: Array<{ type: string; thinking?: string; text?: string }>;
};
const thinkingBlock = translated.content.find((block) => block.type === "thinking");
assert.ok(thinkingBlock, "expected a thinking block carrying the Copilot reasoning_text");
assert.equal(thinkingBlock?.thinking, "Let me think about the user's request before answering.");
});
test("#7856 /v1/messages non-stream translation of a Copilot reasoning_text turn is not flagged malformed", () => {
const translated = translateNonStreamingResponse(copilotStyleResponse, "openai", "claude", null);
const malformedReason = detectMalformedNonStream(translated);
assert.equal(malformedReason, null);
});