mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 07:32:20 +03:00
Validated on the combined batch board + this branch: 231/231 across chatcore-translation-paths, reasoning-cache, strip-reasoning-blobs, and both Responses translator suites. Pre-merge: propagated the #11110/#11129 summary:[] defaults into five assertions here (each commented with its PR) — without it this branch red against the tip, and as a bonus the merge drains the 4 reasoning reds that were live on the tip from those merges. Plaintext now wins over a coexisting opaque companion; opaque-only drops cleanly for plaintext targets; combos keep explicit Skip. Fixes #10949 and #10959. Thank you @jackjinke!
This commit is contained in:
@@ -1 +1 @@
|
||||
- Preserve portable plaintext reasoning by default across streaming and non-streaming Chat Completions and Responses routes while keeping provider-bound opaque state target-compatible. Combos now drop incompatible continuation reasoning by default and can explicitly skip incompatible targets, while known providers no longer show redundant encrypted-reasoning controls. (#10550)
|
||||
- Preserve portable plaintext reasoning by default across streaming and non-streaming Chat Completions and Responses routes while keeping provider-bound opaque state target-compatible. Direct requests drop incompatible continuation reasoning by default; combos can explicitly skip incompatible targets without mutating the request. Known providers no longer show redundant encrypted-reasoning controls. (#10550, #10959)
|
||||
|
||||
1
changelog.d/fixes/10949-mixed-reasoning-plaintext.md
Normal file
1
changelog.d/fixes/10949-mixed-reasoning-plaintext.md
Normal file
@@ -0,0 +1 @@
|
||||
- Preserve explicit plaintext reasoning when a Responses reasoning item also carries opaque provider state (rare OpenCode Go `deepseek-v4-flash` responses). Mixed plaintext + opaque input is projected onto the target transport: plaintext targets keep portable text, opaque targets keep provider state. Opaque-only reasoning is dropped when the selected target cannot replay it, allowing cross-model conversations to continue. (#10949, #10959)
|
||||
@@ -1398,7 +1398,6 @@ export class CodexExecutor extends BaseExecutor {
|
||||
provider: "codex",
|
||||
preserveEncryptedReasoning:
|
||||
credentials?.providerSpecificData?.preserveEncryptedReasoning === true,
|
||||
onIncompatibleReasoning: "drop",
|
||||
});
|
||||
|
||||
if (nativeCodexPassthrough) {
|
||||
|
||||
@@ -516,7 +516,7 @@ export async function handleChatCore({
|
||||
conversationId = null,
|
||||
modelPinned = false,
|
||||
skipResourcePressureGuard = false,
|
||||
reasoningTransportFallback = "skip",
|
||||
reasoningTransportFallback = "drop",
|
||||
managedLease = null,
|
||||
}) {
|
||||
let { provider, model, extendedContext } = modelInfo;
|
||||
@@ -1213,7 +1213,7 @@ export async function handleChatCore({
|
||||
provider,
|
||||
preserveEncryptedReasoning:
|
||||
credentials?.providerSpecificData?.preserveEncryptedReasoning === true,
|
||||
onIncompatibleReasoning: reasoningTransportFallback === "drop" ? "drop" : "reject",
|
||||
onIncompatibleReasoning: reasoningTransportFallback === "skip" ? "reject" : "drop",
|
||||
}
|
||||
);
|
||||
if (policy.incompatibleReasoning) {
|
||||
|
||||
@@ -37,7 +37,6 @@ export interface ReasoningInputPolicyOptions {
|
||||
export interface ReasoningInputPolicyResult {
|
||||
incompatibleReasoning: boolean;
|
||||
}
|
||||
|
||||
export function resolveReasoningTransport(
|
||||
provider: string | null | undefined,
|
||||
preserveEncryptedReasoning = false
|
||||
@@ -47,18 +46,6 @@ export function resolveReasoningTransport(
|
||||
return transport ?? (preserveEncryptedReasoning ? "opaque" : "plaintext");
|
||||
}
|
||||
|
||||
export function createReasoningTransportIncompatibleError(): Error & {
|
||||
statusCode: number;
|
||||
errorType: string;
|
||||
} {
|
||||
const error = new Error(
|
||||
"Reasoning continuation is not compatible with the selected target"
|
||||
) as Error & { statusCode: number; errorType: string };
|
||||
error.statusCode = 400;
|
||||
error.errorType = "reasoning_transport_incompatible";
|
||||
return error;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): JsonRecord | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
|
||||
}
|
||||
@@ -101,11 +88,12 @@ function hasChatPlaintextReasoning(record: JsonRecord): boolean {
|
||||
|
||||
/**
|
||||
* Returns only provider-authentic plaintext continuation state. Display summaries
|
||||
* are excluded, and a record carrying opaque state is never cross-converted.
|
||||
* and opaque-only records are excluded. Explicit plaintext remains independently
|
||||
* portable when the same record also carries an opaque companion (#10949).
|
||||
*/
|
||||
export function extractReplayableResponsesReasoningText(value: unknown): string {
|
||||
const record = asRecord(value);
|
||||
if (!record || record.type !== "reasoning" || hasOpaqueReasoningState(record)) return "";
|
||||
if (!record || record.type !== "reasoning") return "";
|
||||
if (!Array.isArray(record.content)) return "";
|
||||
|
||||
return record.content
|
||||
@@ -307,9 +295,9 @@ function sanitizeResponsesInput(
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies one protocol-independent compatibility decision before request translation.
|
||||
* Plaintext is portable by default; opaque state requires an explicit target declaration.
|
||||
* Display summaries do not affect compatibility; stateless input drops orphan summaries.
|
||||
* Projects reasoning continuation onto the selected target transport.
|
||||
* Incompatible active state is dropped by default; combo routing may reject an
|
||||
* attempt instead so it can fall through without mutating the request.
|
||||
*/
|
||||
export function applyReasoningInputPolicy(
|
||||
body: Record<string, unknown>,
|
||||
@@ -321,14 +309,17 @@ export function applyReasoningInputPolicy(
|
||||
inputFormat === "responses"
|
||||
? inspectResponsesReasoning(body.input)
|
||||
: inspectChatReasoning(body.messages);
|
||||
const incompatibleReasoning = !isReasoningCompatible(inspection, transport);
|
||||
const mixedState = inspection.hasPlaintext && inspection.hasOpaque;
|
||||
const incompatibleReasoning = !mixedState && !isReasoningCompatible(inspection, transport);
|
||||
// Mixed plaintext + opaque input (#10949) is never a rejection: it is projected
|
||||
// onto the target transport by the per-item sanitizers below.
|
||||
|
||||
if (incompatibleReasoning && options.onIncompatibleReasoning !== "drop") {
|
||||
if (incompatibleReasoning && options.onIncompatibleReasoning === "reject") {
|
||||
return { incompatibleReasoning: true };
|
||||
}
|
||||
|
||||
if (inputFormat === "chat") {
|
||||
if (incompatibleReasoning && Array.isArray(body.messages)) {
|
||||
if ((incompatibleReasoning || mixedState) && Array.isArray(body.messages)) {
|
||||
body.messages = dropIncompatibleChatReasoning(body.messages, transport);
|
||||
}
|
||||
return { incompatibleReasoning: false };
|
||||
@@ -343,12 +334,13 @@ export function applyReasoningInputPolicy(
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!Array.isArray(body.input)) return { incompatibleReasoning: false };
|
||||
body.input = sanitizeResponsesInput(
|
||||
body.input,
|
||||
transport,
|
||||
incompatibleReasoning,
|
||||
body.store === false
|
||||
);
|
||||
if (Array.isArray(body.input)) {
|
||||
body.input = sanitizeResponsesInput(
|
||||
body.input,
|
||||
transport,
|
||||
incompatibleReasoning || mixedState,
|
||||
body.store === false
|
||||
);
|
||||
}
|
||||
return { incompatibleReasoning: false };
|
||||
}
|
||||
|
||||
@@ -8,11 +8,7 @@ import { isOpenAIResponsesStoreEnabled } from "@/lib/providers/requestDefaults";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { register } from "../registry.ts";
|
||||
import { normalizeResponsesInputForChat } from "../../utils/responsesInputNormalization.ts";
|
||||
import {
|
||||
createReasoningTransportIncompatibleError,
|
||||
hasOpaqueReasoningState,
|
||||
extractReplayableResponsesReasoningText,
|
||||
} from "../../services/reasoningInputPolicy.ts";
|
||||
import { extractReplayableResponsesReasoningText } from "../../services/reasoningInputPolicy.ts";
|
||||
import {
|
||||
getRegisteredProviders,
|
||||
requiresPlainStringContent,
|
||||
@@ -454,10 +450,8 @@ export function openaiResponsesToOpenAIRequest(
|
||||
|
||||
if (itemType === "reasoning") {
|
||||
// Only genuine plaintext reasoning can cross into Chat reasoning_content.
|
||||
// Opaque encrypted state and its display summary have no Chat replay form.
|
||||
if (preserveReasoningContent && hasOpaqueReasoningState(item)) {
|
||||
throw createReasoningTransportIncompatibleError();
|
||||
}
|
||||
// Opaque encrypted state and its display summary have no Chat replay form,
|
||||
// so opaque-only items are dropped while mixed items replay their plaintext.
|
||||
if (preserveReasoningContent) {
|
||||
const reasoning = extractReplayableResponsesReasoningText(item);
|
||||
if (reasoning) {
|
||||
|
||||
@@ -3896,15 +3896,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
{config.reasoningTransportFallback !== "skip" && (
|
||||
<p className="text-[10px] text-amber-600 dark:text-amber-300 mt-1">
|
||||
{getI18nOrFallback(
|
||||
t,
|
||||
"reasoningTransportFallbackDropWarning",
|
||||
"May lose continuation context or cause tool-call continuations to fail."
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabelWithHelp
|
||||
|
||||
@@ -1851,7 +1851,7 @@ async function handleSingleModelChat(
|
||||
modelPinned: runtimeOptions?.modelPinned ?? false,
|
||||
routingComboId: runtimeOptions?.routingComboId ?? null,
|
||||
sessionAffinityKey: runtimeOptions.sessionAffinityKey ?? null,
|
||||
reasoningTransportFallback: runtimeOptions.reasoningTransportFallback ?? "skip",
|
||||
reasoningTransportFallback: runtimeOptions.reasoningTransportFallback ?? "drop",
|
||||
managedLease: runtimeOptions.managedLease ?? null,
|
||||
},
|
||||
runtimeOptions
|
||||
|
||||
@@ -422,7 +422,7 @@ export async function executeChatWithBreaker({
|
||||
conversationId = null,
|
||||
modelPinned = false,
|
||||
routingComboId = null,
|
||||
reasoningTransportFallback = "skip",
|
||||
reasoningTransportFallback = "drop",
|
||||
sessionAffinityKey = null,
|
||||
managedLease = null,
|
||||
}: ExecuteChatWithBreakerOptions): Promise<ExecuteChatWithBreakerResult> {
|
||||
|
||||
@@ -369,7 +369,7 @@ async function invokeChatCore({
|
||||
onCredentialsRefreshed = null,
|
||||
onRequestSuccess = null,
|
||||
sessionAffinityKey = null,
|
||||
reasoningTransportFallback = "skip",
|
||||
reasoningTransportFallback = "drop",
|
||||
managedLease = null,
|
||||
cachedSettings = null,
|
||||
}: any = {}) {
|
||||
@@ -631,7 +631,7 @@ test("chatCore translates a streaming Responses upstream for a Chat client", asy
|
||||
assert.match(streamed, /"content":"ok"/);
|
||||
assert.match(streamed, /data: \[DONE\]/);
|
||||
});
|
||||
test("chatCore rejects opaque reasoning for unknown Responses targets unless explicitly enabled", async () => {
|
||||
test("chatCore drops opaque reasoning for plaintext Responses targets by default (#10959)", async () => {
|
||||
const reasoningItems = [
|
||||
{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" },
|
||||
{ type: "reasoning", encrypted_content: "" },
|
||||
@@ -640,7 +640,7 @@ test("chatCore rejects opaque reasoning for unknown Responses targets unless exp
|
||||
{ id: "fc_call", type: "function_call", call_id: "call_1", name: "search", arguments: "{}" },
|
||||
];
|
||||
|
||||
const rejected = await invokeChatCore({
|
||||
const dropped = await invokeChatCore({
|
||||
provider: "openai-compatible-sp-openai",
|
||||
model: "gpt-5.4",
|
||||
endpoint: "/v1/responses",
|
||||
@@ -656,9 +656,12 @@ test("chatCore rejects opaque reasoning for unknown Responses targets unless exp
|
||||
responseFormat: "openai-responses",
|
||||
});
|
||||
|
||||
assert.equal(rejected.result.success, false);
|
||||
assert.equal(rejected.result.status, 400);
|
||||
assert.equal(rejected.calls.length, 0);
|
||||
assert.equal(dropped.result.success, true);
|
||||
assert.equal(dropped.calls.length, 1);
|
||||
assert.deepEqual(
|
||||
dropped.call.body.input.filter((item) => item.type === "reasoning"),
|
||||
[{ type: "reasoning", summary: [{ text: "not self-contained" }] }]
|
||||
);
|
||||
|
||||
const enabled = await invokeChatCore({
|
||||
provider: "openai-compatible-sp-openai",
|
||||
@@ -682,7 +685,7 @@ test("chatCore rejects opaque reasoning for unknown Responses targets unless exp
|
||||
assert.deepEqual(
|
||||
input.filter((item) => item.type === "reasoning"),
|
||||
[
|
||||
{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" },
|
||||
{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob", summary: [] }, // summary defaulted by #11110
|
||||
{ type: "reasoning", summary: [{ text: "not self-contained" }] },
|
||||
]
|
||||
);
|
||||
@@ -693,9 +696,9 @@ test("chatCore rejects opaque reasoning for unknown Responses targets unless exp
|
||||
assert.equal(input.find((item) => item.type === "function_call")?.id, undefined);
|
||||
});
|
||||
|
||||
test("chatCore applies Chat reasoning compatibility before stream mode diverges", async () => {
|
||||
test("chatCore drops incompatible Chat reasoning before stream mode diverges (#10959)", async () => {
|
||||
for (const stream of [false, true]) {
|
||||
const rejected = await invokeChatCore({
|
||||
const dropped = await invokeChatCore({
|
||||
provider: "openai-compatible-sp-openai",
|
||||
model: "gpt-5.4",
|
||||
endpoint: "/v1/chat/completions",
|
||||
@@ -728,14 +731,14 @@ test("chatCore applies Chat reasoning compatibility before stream mode diverges"
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(rejected.result.success, false, `stream=${stream}`);
|
||||
assert.equal(rejected.result.status, 400, `stream=${stream}`);
|
||||
assert.equal(rejected.calls.length, 0, `stream=${stream}`);
|
||||
assert.equal(dropped.result.success, true, `stream=${stream}`);
|
||||
assert.equal(dropped.calls.length, 1, `stream=${stream}`);
|
||||
assert.equal(dropped.call.body.messages[0].reasoning_details, undefined, `stream=${stream}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("chatCore can drop incompatible reasoning for an opted-in Combo attempt", async () => {
|
||||
const dropped = await invokeChatCore({
|
||||
test("chatCore preserves Combo skip behavior for incompatible reasoning", async () => {
|
||||
const skipped = await invokeChatCore({
|
||||
provider: "openai-compatible-sp-openai",
|
||||
model: "gpt-5.4",
|
||||
endpoint: "/v1/responses",
|
||||
@@ -757,15 +760,12 @@ test("chatCore can drop incompatible reasoning for an opted-in Combo attempt", a
|
||||
},
|
||||
responseFormat: "openai-responses",
|
||||
isCombo: true,
|
||||
reasoningTransportFallback: "drop",
|
||||
reasoningTransportFallback: "skip",
|
||||
});
|
||||
|
||||
assert.equal(dropped.result.success, true);
|
||||
assert.equal(dropped.calls.length, 1);
|
||||
assert.equal(
|
||||
dropped.call.body.input.some((item) => item.type === "reasoning"),
|
||||
false
|
||||
);
|
||||
assert.equal(skipped.result.success, false);
|
||||
assert.equal(skipped.result.status, 400);
|
||||
assert.equal(skipped.calls.length, 0);
|
||||
});
|
||||
|
||||
test("chatCore carries Chat reasoning_content into official DeepSeek Responses input", async () => {
|
||||
@@ -800,6 +800,7 @@ test("chatCore carries Chat reasoning_content into official DeepSeek Responses i
|
||||
assert.deepEqual(call.body.input.slice(0, 3), [
|
||||
{
|
||||
type: "reasoning",
|
||||
summary: [], // defaulted on freshly-built reasoning items (#11129)
|
||||
content: [{ type: "reasoning_text", text: "Inspect before calling the tool" }],
|
||||
},
|
||||
{
|
||||
@@ -866,7 +867,7 @@ test("chatCore replays nonstream DeepSeek Responses reasoning across a Chat tool
|
||||
assert.equal(second.result.success, true);
|
||||
assert.deepEqual(
|
||||
second.call.body.input.find((item) => item.type === "reasoning"),
|
||||
{ type: "reasoning", content: [{ type: "reasoning_text", text: reasoning }] }
|
||||
{ type: "reasoning", content: [{ type: "reasoning_text", text: reasoning }], summary: [] } // summary defaulted by #11129
|
||||
);
|
||||
});
|
||||
|
||||
@@ -930,7 +931,7 @@ test("chatCore replays streamed DeepSeek Responses reasoning across a Chat tool
|
||||
assert.equal(second.result.success, true);
|
||||
assert.deepEqual(
|
||||
second.call.body.input.find((item) => item.type === "reasoning"),
|
||||
{ type: "reasoning", content: [{ type: "reasoning_text", text: reasoning }] }
|
||||
{ type: "reasoning", content: [{ type: "reasoning_text", text: reasoning }], summary: [] } // summary defaulted by #11129
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1132,7 +1133,7 @@ test("chatCore automatically preserves provider-generated opaque reasoning for C
|
||||
assert.equal(result.success, true);
|
||||
assert.deepEqual(
|
||||
call.body.input.filter((item) => item.type === "reasoning"),
|
||||
[{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" }]
|
||||
[{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob", summary: [] }] // summary defaulted by #11110
|
||||
);
|
||||
assert.equal(
|
||||
call.body.input.some((item) => item.type === "item_reference"),
|
||||
|
||||
@@ -752,47 +752,70 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
assert.equal(lookupReasoning(callId), "Authentic provider reasoning");
|
||||
});
|
||||
|
||||
it("should never cache Responses summaries or opaque plaintext companions", () => {
|
||||
for (const [suffix, reasoningItem] of [
|
||||
[
|
||||
"summary",
|
||||
{
|
||||
type: "reasoning",
|
||||
summary: [{ type: "summary_text", text: "Display-only summary" }],
|
||||
},
|
||||
],
|
||||
[
|
||||
"mixed",
|
||||
{
|
||||
type: "reasoning",
|
||||
encrypted_content: "opaque-provider-state",
|
||||
content: [{ type: "reasoning_text", text: "Unsafe plaintext companion" }],
|
||||
summary: [{ type: "summary_text", text: "Display-only mixed summary" }],
|
||||
},
|
||||
],
|
||||
] as const) {
|
||||
clearReasoningCacheAll();
|
||||
const callId = `call_nonstream_${suffix}_reasoning`;
|
||||
const translated = translateNonStreamingResponse(
|
||||
{
|
||||
object: "response",
|
||||
model: "deepseek-v4-flash",
|
||||
output: [
|
||||
reasoningItem,
|
||||
{ type: "function_call", call_id: callId, name: "read_file", arguments: "{}" },
|
||||
],
|
||||
},
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
FORMATS.OPENAI
|
||||
) as { choices?: Array<{ message?: Record<string, unknown> }> };
|
||||
const message = translated.choices?.[0]?.message;
|
||||
it("preserves plaintext reasoning from a mixed plaintext + encrypted_content item (#10949)", () => {
|
||||
clearReasoningCacheAll();
|
||||
const callId = "call_nonstream_mixed_reasoning";
|
||||
const translated = translateNonStreamingResponse(
|
||||
{
|
||||
object: "response",
|
||||
model: "deepseek-v4-flash",
|
||||
output: [
|
||||
{
|
||||
type: "reasoning",
|
||||
content: [
|
||||
{
|
||||
type: "reasoning_text",
|
||||
text: "Let me start by reading the directory to understand the structure of the corpus.",
|
||||
},
|
||||
],
|
||||
encrypted_content: "<opaque state>",
|
||||
summary: [],
|
||||
},
|
||||
{ type: "function_call", call_id: callId, name: "read_file", arguments: "{}" },
|
||||
],
|
||||
},
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
FORMATS.OPENAI
|
||||
) as { choices?: Array<{ message?: Record<string, unknown> }> };
|
||||
const message = translated.choices?.[0]?.message;
|
||||
|
||||
assert.ok(message);
|
||||
assert.equal(message.reasoning_content, undefined);
|
||||
assert.ok(Array.isArray(message.reasoning_summary));
|
||||
assert.equal(cacheReasoningFromAssistantMessage(message, "deepseek", "deepseek-v4-flash"), 0);
|
||||
assert.equal(lookupReasoning(callId), null);
|
||||
}
|
||||
assert.ok(message);
|
||||
assert.equal(
|
||||
message.reasoning_content,
|
||||
"Let me start by reading the directory to understand the structure of the corpus."
|
||||
);
|
||||
assert.equal(cacheReasoningFromAssistantMessage(message, "deepseek", "deepseek-v4-flash"), 1);
|
||||
assert.equal(
|
||||
lookupReasoning(callId),
|
||||
"Let me start by reading the directory to understand the structure of the corpus."
|
||||
);
|
||||
});
|
||||
|
||||
it("should never cache summary-only Responses reasoning", () => {
|
||||
clearReasoningCacheAll();
|
||||
const callId = "call_nonstream_summary_reasoning";
|
||||
const translated = translateNonStreamingResponse(
|
||||
{
|
||||
object: "response",
|
||||
model: "deepseek-v4-flash",
|
||||
output: [
|
||||
{
|
||||
type: "reasoning",
|
||||
summary: [{ type: "summary_text", text: "Display-only summary" }],
|
||||
},
|
||||
{ type: "function_call", call_id: callId, name: "read_file", arguments: "{}" },
|
||||
],
|
||||
},
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
FORMATS.OPENAI
|
||||
) as { choices?: Array<{ message?: Record<string, unknown> }> };
|
||||
const message = translated.choices?.[0]?.message;
|
||||
|
||||
assert.ok(message);
|
||||
assert.equal(message.reasoning_content, undefined);
|
||||
assert.ok(Array.isArray(message.reasoning_summary));
|
||||
assert.equal(cacheReasoningFromAssistantMessage(message, "deepseek", "deepseek-v4-flash"), 0);
|
||||
assert.equal(lookupReasoning(callId), null);
|
||||
});
|
||||
|
||||
it("should preserve client-provided reasoning content", () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { omitEncryptedReasoningForLog } from "../../src/lib/logPayloads.ts";
|
||||
// Responses reasoning replay is target-scoped. Plaintext DeepSeek state and
|
||||
// provider-generated opaque state are never interchangeable.
|
||||
|
||||
test("unknown Responses targets reject opaque reasoning and ignore display summaries", () => {
|
||||
test("unknown Responses targets drop opaque reasoning and preserve display summaries (#10959)", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
input: [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
|
||||
@@ -24,11 +24,14 @@ test("unknown Responses targets reject opaque reasoning and ignore display summa
|
||||
],
|
||||
};
|
||||
|
||||
const originalInput = structuredClone(body.input);
|
||||
const result = applyReasoningInputPolicy(body, "responses");
|
||||
|
||||
assert.equal(result.incompatibleReasoning, true);
|
||||
assert.deepEqual(body.input, originalInput, "rejection must not mutate the request");
|
||||
assert.equal(result.incompatibleReasoning, false);
|
||||
assert.deepEqual(body.input, [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
|
||||
{ type: "reasoning", summary: [{ text: "display only" }] },
|
||||
{ type: "function_call", name: "search", arguments: "{}", call_id: "call_1" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("unannotated targets preserve plaintext Responses reasoning without synthetic IDs", () => {
|
||||
@@ -132,7 +135,7 @@ test("Chat drop removes opaque state while preserving plaintext and summary deta
|
||||
]);
|
||||
});
|
||||
|
||||
test("DeepSeek rejects plaintext reasoning carrying opaque provider state", () => {
|
||||
test("DeepSeek projects plaintext reasoning carrying opaque provider state onto the plaintext transport (#10949)", () => {
|
||||
for (const opaqueField of ["signature", "format"] as const) {
|
||||
const body: Record<string, unknown> = {
|
||||
input: [
|
||||
@@ -148,13 +151,11 @@ test("DeepSeek rejects plaintext reasoning carrying opaque provider state", () =
|
||||
|
||||
const result = applyReasoningInputPolicy(body, "responses", { provider: "deepseek" });
|
||||
|
||||
assert.equal(result.incompatibleReasoning, true, opaqueField);
|
||||
assert.equal(result.incompatibleReasoning, false, opaqueField);
|
||||
assert.deepEqual(body.input, [
|
||||
{
|
||||
id: "rs_mixed123",
|
||||
type: "reasoning",
|
||||
content: [{ type: "reasoning_text", text: "untrusted companion" }],
|
||||
[opaqueField]: "provider-state",
|
||||
},
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
|
||||
]);
|
||||
@@ -199,6 +200,49 @@ test("drop fallback removes only the incompatible active transport and preserves
|
||||
assert.equal(opaqueReasoning.encrypted_content, "provider-state");
|
||||
});
|
||||
|
||||
test("mixed plaintext + opaque reasoning follows the target transport instead of rejecting (#10949)", () => {
|
||||
const mixedReasoning = {
|
||||
id: "rs_mixed",
|
||||
type: "reasoning",
|
||||
content: [{ type: "reasoning_text", text: "inspect first" }],
|
||||
encrypted_content: "provider-state",
|
||||
summary: [{ type: "summary_text", text: "display only" }],
|
||||
};
|
||||
|
||||
// Plaintext target (deepseek): keep the portable plaintext, strip opaque state.
|
||||
const toPlaintext: Record<string, unknown> = {
|
||||
input: [structuredClone(mixedReasoning)],
|
||||
};
|
||||
const plaintextResult = applyReasoningInputPolicy(toPlaintext, "responses", {
|
||||
provider: "deepseek",
|
||||
});
|
||||
assert.equal(plaintextResult.incompatibleReasoning, false);
|
||||
assert.deepEqual(toPlaintext.input, [
|
||||
{
|
||||
type: "reasoning",
|
||||
content: [{ type: "reasoning_text", text: "inspect first" }],
|
||||
summary: [{ type: "summary_text", text: "display only" }],
|
||||
},
|
||||
]);
|
||||
|
||||
// Opaque target (openai): keep the provider state, strip the plaintext.
|
||||
const toOpaque: Record<string, unknown> = {
|
||||
input: [structuredClone(mixedReasoning)],
|
||||
};
|
||||
const opaqueResult = applyReasoningInputPolicy(toOpaque, "responses", {
|
||||
provider: "openai",
|
||||
});
|
||||
assert.equal(opaqueResult.incompatibleReasoning, false);
|
||||
assert.deepEqual(toOpaque.input, [
|
||||
{
|
||||
id: "rs_mixed",
|
||||
type: "reasoning",
|
||||
encrypted_content: "provider-state",
|
||||
summary: [{ type: "summary_text", text: "display only" }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("drop fallback preserves reasoning when its transport is compatible", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
input: [
|
||||
|
||||
@@ -172,28 +172,26 @@ test("Responses -> Chat keeps summary-only reasoning out of continuation state",
|
||||
assert.equal(result.messages[0].reasoning_content, undefined);
|
||||
});
|
||||
|
||||
test("Responses -> Chat rejects opaque reasoning instead of replaying its plaintext companion", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
openaiResponsesToOpenAIRequest(
|
||||
"deepseek-v4-pro",
|
||||
test("Responses -> Chat replays the plaintext companion of an opaque reasoning item (#10949)", () => {
|
||||
const result = openaiResponsesToOpenAIRequest(
|
||||
"deepseek-v4-pro",
|
||||
{
|
||||
input: [
|
||||
{
|
||||
input: [
|
||||
{
|
||||
id: "rs_opaque",
|
||||
type: "reasoning",
|
||||
encrypted_content: "opaque-provider-state",
|
||||
content: [{ type: "reasoning_text", text: "Untrusted plaintext companion" }],
|
||||
summary: [{ type: "summary_text", text: "Display summary" }],
|
||||
},
|
||||
{ type: "function_call", call_id: "call_1", name: "search", arguments: "{}" },
|
||||
],
|
||||
id: "rs_opaque",
|
||||
type: "reasoning",
|
||||
encrypted_content: "opaque-provider-state",
|
||||
content: [{ type: "reasoning_text", text: "Untrusted plaintext companion" }],
|
||||
summary: [{ type: "summary_text", text: "Display summary" }],
|
||||
},
|
||||
false,
|
||||
{ _preserveReasoningContent: true }
|
||||
),
|
||||
/Reasoning continuation is not compatible/
|
||||
);
|
||||
{ type: "function_call", call_id: "call_1", name: "search", arguments: "{}" },
|
||||
],
|
||||
},
|
||||
false,
|
||||
{ _preserveReasoningContent: true }
|
||||
) as { messages: Array<Record<string, unknown>> };
|
||||
|
||||
assert.equal(result.messages[0].reasoning_content, "Untrusted plaintext companion");
|
||||
});
|
||||
|
||||
test("Responses -> Chat merges assistant text that follows a function call", () => {
|
||||
|
||||
@@ -423,6 +423,52 @@ test("Responses -> OpenAI: preserves non-object Read JSON-string arguments", ()
|
||||
assert.equal(done.choices[0].delta.tool_calls[0].function.arguments, "null");
|
||||
});
|
||||
|
||||
test("Responses -> OpenAI: mixed plaintext + encrypted_content reasoning replays its plaintext (#10949)", () => {
|
||||
const state = {};
|
||||
const done = openaiResponsesToOpenAIResponse(
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: {
|
||||
type: "reasoning",
|
||||
id: "rs_mixed",
|
||||
content: [
|
||||
{
|
||||
type: "reasoning_text",
|
||||
text: "Let me start by reading the directory to understand the structure of the corpus.",
|
||||
},
|
||||
],
|
||||
encrypted_content: "<opaque state>",
|
||||
summary: [],
|
||||
},
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.ok(done, "mixed reasoning item must surface a delta");
|
||||
assert.equal(
|
||||
done.choices[0].delta.reasoning_content,
|
||||
"Let me start by reading the directory to understand the structure of the corpus."
|
||||
);
|
||||
});
|
||||
|
||||
test("Responses -> OpenAI: opaque-only reasoning still emits no fabricated plaintext", () => {
|
||||
const state = {};
|
||||
const done = openaiResponsesToOpenAIResponse(
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: {
|
||||
type: "reasoning",
|
||||
id: "rs_opaque_only",
|
||||
encrypted_content: "<opaque state>",
|
||||
summary: [],
|
||||
},
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.equal(done, null);
|
||||
});
|
||||
|
||||
test("Responses -> OpenAI: strips empty optional args from JSON-string output_item.done arguments", () => {
|
||||
const state = {};
|
||||
openaiResponsesToOpenAIResponse(
|
||||
|
||||
Reference in New Issue
Block a user