mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Hide internal reasoning replay placeholders (#7912)
* fix: hide internal reasoning replay placeholder * fix: hide internal reasoning replay placeholder in OpenAI→Claude translation too Mirror the isInternalReasoningPlaceholder() guard already applied to the Responses-API and OpenAI-Responses reasoning paths in openaiToClaudeResponse() (open-sse/translator/response/openai-to-claude.ts). The internal reasoning-replay sentinel was still leaking into the Claude "thinking" content block on this translation path. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test: close both-add merge of #7912 and #7905 reasoning/tool tests Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { appendToolCallArgumentDelta } from "../utils/toolCallArguments.ts";
|
||||
import { shouldParseTextualReasoningTags } from "../handlers/responseSanitizer.ts";
|
||||
import { isInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
/**
|
||||
@@ -525,7 +526,7 @@ export function createResponsesApiTransformStream(
|
||||
}
|
||||
|
||||
// Handle reasoning_content (OpenAI native format)
|
||||
if (delta.reasoning_content) {
|
||||
if (delta.reasoning_content && !isInternalReasoningPlaceholder(delta.reasoning_content)) {
|
||||
startReasoning(controller, idx);
|
||||
emitReasoningDelta(controller, delta.reasoning_content);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
|
||||
import { lookupReasoning, recordReplay } from "../../services/reasoningCache.ts";
|
||||
import { getModelTargetFormat } from "../../config/providerModels.ts";
|
||||
import { NON_ANTHROPIC_THINKING_PLACEHOLDER } from "../../utils/reasoningPlaceholder.ts";
|
||||
|
||||
export { NON_ANTHROPIC_THINKING_PLACEHOLDER } from "../../utils/reasoningPlaceholder.ts";
|
||||
|
||||
// MiniMax exposes a Claude-compatible endpoint but rejects Anthropic's extended
|
||||
// `output_config` parameter (used to steer reasoning effort and structured output)
|
||||
@@ -9,10 +12,7 @@ import { getModelTargetFormat } from "../../config/providerModels.ts";
|
||||
// dispatching Claude-shape requests to these providers. Anthropic Claude and
|
||||
// other Claude-compatible upstreams that do accept it are unaffected.
|
||||
// Ported from upstream decolua/9router#820 by @hiepau1231.
|
||||
const CLAUDE_FORMAT_PROVIDERS_WITHOUT_OUTPUT_CONFIG = new Set<string>([
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
]);
|
||||
const CLAUDE_FORMAT_PROVIDERS_WITHOUT_OUTPUT_CONFIG = new Set<string>(["minimax", "minimax-cn"]);
|
||||
|
||||
// Placeholder thinking text used as last-resort fallback when:
|
||||
// - Target upstream is a non-Anthropic Claude-shape provider
|
||||
@@ -21,8 +21,6 @@ const CLAUDE_FORMAT_PROVIDERS_WITHOUT_OUTPUT_CONFIG = new Set<string>([
|
||||
// - reasoningCache has no entry for the corresponding tool_use.id
|
||||
// Must be non-empty: kimi-coding treats empty `thinking.thinking` as
|
||||
// `reasoning_content missing` and 400s.
|
||||
export const NON_ANTHROPIC_THINKING_PLACEHOLDER = "(prior reasoning summary unavailable)";
|
||||
|
||||
type ClaudeContentBlock = {
|
||||
type?: string;
|
||||
text?: string;
|
||||
@@ -468,7 +466,11 @@ export function prepareClaudeRequest(
|
||||
? typeof b.data === "string" && (b.data as string).length > 0
|
||||
: typeof b.signature === "string" && b.signature.length > 0
|
||||
);
|
||||
if (latestHasExistingThinking && supportsRedactedThinking && latestHasGenuineThinkingSignature) {
|
||||
if (
|
||||
latestHasExistingThinking &&
|
||||
supportsRedactedThinking &&
|
||||
latestHasGenuineThinkingSignature
|
||||
) {
|
||||
// Anthropic: skip all thinking-block rewrites entirely — the
|
||||
// blocks must remain verbatim (type, thinking, signature, data).
|
||||
continue;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FORMATS } from "../formats.ts";
|
||||
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
|
||||
import { fallbackToolCallId } from "../helpers/toolCallHelper.ts";
|
||||
import { shouldParseTextualReasoningTags } from "../../handlers/responseSanitizer.ts";
|
||||
import { isInternalReasoningPlaceholder } from "../../utils/reasoningPlaceholder.ts";
|
||||
import {
|
||||
normalizeToolName,
|
||||
stripEmptyOptionalToolArgs,
|
||||
@@ -162,7 +163,7 @@ export function openaiToOpenAIResponsesResponse(chunk, state) {
|
||||
});
|
||||
}
|
||||
|
||||
if (delta.reasoning_content) {
|
||||
if (delta.reasoning_content && !isInternalReasoningPlaceholder(delta.reasoning_content)) {
|
||||
startReasoning(state, emit, idx);
|
||||
emitReasoningDelta(state, emit, delta.reasoning_content);
|
||||
}
|
||||
@@ -646,6 +647,7 @@ function markResponsesReasoningDeltaEmitted(state, itemId) {
|
||||
// its thinking panel (`reasoning_content`, or `reasoning_text` for Copilot-compatible
|
||||
// clients). Mirrors the `response.reasoning_summary_text.delta` branch.
|
||||
function buildResponsesReasoningDeltaChunk(state, text) {
|
||||
if (isInternalReasoningPlaceholder(text)) return null;
|
||||
const delta = state.copilotCompatibleReasoning
|
||||
? { reasoning_text: text }
|
||||
: { reasoning_content: text };
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CLAUDE_OAUTH_TOOL_PREFIX } from "../request/openai-to-claude.ts";
|
||||
import { hasToolCallShim, applyToolCallShimToBuffer } from "../helpers/toolCallShim.ts";
|
||||
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
|
||||
import { isAbortFinishReason } from "../../utils/finishReason.ts";
|
||||
import { isInternalReasoningPlaceholder } from "../../utils/reasoningPlaceholder.ts";
|
||||
|
||||
// Helper: stop thinking block if started
|
||||
function stopThinkingBlock(state, results) {
|
||||
@@ -108,7 +109,7 @@ export function openaiToClaudeResponse(chunk, state) {
|
||||
}
|
||||
if (parts.length > 0) reasoningContent = parts.join("");
|
||||
}
|
||||
if (reasoningContent) {
|
||||
if (reasoningContent && !isInternalReasoningPlaceholder(reasoningContent)) {
|
||||
stopTextBlock(state, results);
|
||||
|
||||
if (!state.thinkingBlockStarted) {
|
||||
|
||||
10
open-sse/utils/reasoningPlaceholder.ts
Normal file
10
open-sse/utils/reasoningPlaceholder.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Internal replay sentinel used when an upstream requires non-empty reasoning content but the
|
||||
* original reasoning summary is unavailable. It is valid request scaffolding, never user-visible
|
||||
* reasoning, so response translators must suppress it before emitting client-facing events.
|
||||
*/
|
||||
export const NON_ANTHROPIC_THINKING_PLACEHOLDER = "(prior reasoning summary unavailable)";
|
||||
|
||||
export function isInternalReasoningPlaceholder(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim() === NON_ANTHROPIC_THINKING_PLACEHOLDER;
|
||||
}
|
||||
@@ -175,6 +175,21 @@ test("createResponsesApiTransformStream handles native reasoning content and too
|
||||
);
|
||||
});
|
||||
|
||||
test("createResponsesApiTransformStream hides the internal reasoning replay placeholder", async () => {
|
||||
const output = await runTransformStream([
|
||||
'data: {"choices":[{"index":0,"delta":{"reasoning_content":"(prior reasoning summary unavailable)"}}]}\n\n',
|
||||
'data: {"choices":[{"index":0,"delta":{"content":"Visible answer"},"finish_reason":null}]}\n\n',
|
||||
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n',
|
||||
]);
|
||||
|
||||
const events = parseSseOutput(output);
|
||||
assert.equal(
|
||||
events.some((event) => event.event === "response.reasoning_summary_text.delta"),
|
||||
false
|
||||
);
|
||||
assert.equal(output.includes("prior reasoning summary unavailable"), false);
|
||||
assert.equal(output.includes("Visible answer"), true);
|
||||
});
|
||||
test("createResponsesApiTransformStream restores declared custom tools without changing functions", async () => {
|
||||
const output = await runTransformStream(
|
||||
[
|
||||
|
||||
@@ -544,6 +544,50 @@ test("Responses→Chat streaming: reasoning delta emits reasoning_content in Cha
|
||||
assert.equal(result.choices[0].delta.reasoning_content, "thinking step...");
|
||||
});
|
||||
|
||||
test("Responses→Chat streaming: internal reasoning replay placeholder stays hidden", () => {
|
||||
const state = {
|
||||
started: false,
|
||||
chatId: null,
|
||||
created: null,
|
||||
toolCallIndex: 0,
|
||||
finishReasonSent: false,
|
||||
};
|
||||
|
||||
const result = openaiResponsesToOpenAIResponse(
|
||||
{
|
||||
type: "response.reasoning_summary_text.delta",
|
||||
delta: "(prior reasoning summary unavailable)",
|
||||
item_id: "rs_1",
|
||||
output_index: 0,
|
||||
summary_index: 0,
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("Chat→Responses streaming: internal reasoning replay placeholder stays hidden", () => {
|
||||
const state = initState(FORMATS.OPENAI_RESPONSES);
|
||||
const events = openaiToOpenAIResponsesResponse(
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: "(prior reasoning summary unavailable)" },
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
events.some((event) => event.event === "response.reasoning_summary_text.delta"),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("Responses→Chat streaming: Copilot mode emits reasoning_text for summary deltas", () => {
|
||||
const state = {
|
||||
started: false,
|
||||
|
||||
@@ -76,6 +76,41 @@ test("OpenAI stream: reasoning_content closes before text content starts", () =>
|
||||
assert.equal(result[5].delta.text, "Answer");
|
||||
});
|
||||
|
||||
test("OpenAI stream: internal reasoning replay placeholder stays hidden from Claude thinking block", () => {
|
||||
const state = createState();
|
||||
const placeholder = openaiToClaudeResponse(
|
||||
{
|
||||
id: "chatcmpl-2b",
|
||||
model: "gpt-4.1",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: "(prior reasoning summary unavailable)" },
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
state
|
||||
);
|
||||
const text = openaiToClaudeResponse(
|
||||
{
|
||||
id: "chatcmpl-2b",
|
||||
model: "gpt-4.1",
|
||||
choices: [{ index: 0, delta: { content: "Answer" }, finish_reason: null }],
|
||||
},
|
||||
state
|
||||
);
|
||||
const result = flatten([placeholder, text]);
|
||||
|
||||
assert.equal(
|
||||
result.some((event) => event.type === "content_block_start" && event.content_block?.type === "thinking"),
|
||||
false
|
||||
);
|
||||
assert.equal(result[0].type, "message_start");
|
||||
assert.equal(result[1].content_block.type, "text");
|
||||
assert.equal(result[2].delta.text, "Answer");
|
||||
});
|
||||
|
||||
test("OpenAI stream: tool calls strip Claude OAuth prefix and keep cache usage", () => {
|
||||
const state = createState();
|
||||
const started = openaiToClaudeResponse(
|
||||
|
||||
Reference in New Issue
Block a user