mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
Signed-off-by: Minxi Hou <houminxi@gmail.com>
This commit is contained in:
@@ -43,7 +43,19 @@ export function resolveReasoningTransport(
|
||||
): ReasoningTransport {
|
||||
const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
|
||||
const transport = REASONING_TRANSPORTS.get(normalized);
|
||||
return transport ?? (preserveEncryptedReasoning ? "opaque" : "plaintext");
|
||||
if (transport) return transport;
|
||||
// #12128: Generic Responses-protocol endpoints (e.g. openai-compatible-responses-*,
|
||||
// custom-openai-responses, proxy backends) implement the OpenAI/Codex Responses API
|
||||
// where reasoning input items cannot accept plaintext content (maxItems: 0).
|
||||
if (
|
||||
normalized.startsWith("openai-compatible-responses") ||
|
||||
normalized.startsWith("custom-openai-responses") ||
|
||||
normalized.includes("codex") ||
|
||||
normalized.includes("responses")
|
||||
) {
|
||||
return "opaque";
|
||||
}
|
||||
return preserveEncryptedReasoning ? "opaque" : "plaintext";
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): JsonRecord | null {
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
normalizeResponsesReasoningEffort,
|
||||
RESPONSES_STORE_MARKER,
|
||||
} from "./request/openai-responses/helpers.ts";
|
||||
import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts";
|
||||
|
||||
bootstrapTranslatorRegistry();
|
||||
export { register } from "./registry.ts";
|
||||
@@ -575,6 +576,14 @@ export function translateRequest(
|
||||
// Normalize openai-responses input shape for providers that require list input.
|
||||
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
|
||||
result = normalizeOpenAIResponsesRequest(result);
|
||||
// #12128: Sanitize reasoning input items for Responses targets (strip plaintext content for opaque backends)
|
||||
applyReasoningInputPolicy(result as Record<string, unknown>, "responses", {
|
||||
provider,
|
||||
preserveEncryptedReasoning:
|
||||
(credentials as { providerSpecificData?: { preserveEncryptedReasoning?: boolean } } | null)
|
||||
?.providerSpecificData?.preserveEncryptedReasoning === true,
|
||||
onIncompatibleReasoning: "drop",
|
||||
});
|
||||
}
|
||||
|
||||
// Second role normalization: only for OPENAI_RESPONSES. Here messages are built from input
|
||||
|
||||
88
tests/unit/responses-plaintext-reasoning-input-12128.test.ts
Normal file
88
tests/unit/responses-plaintext-reasoning-input-12128.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
||||
const { resolveReasoningTransport, applyReasoningInputPolicy } = await import(
|
||||
"../../open-sse/services/reasoningInputPolicy.ts"
|
||||
);
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
|
||||
test("#12128: resolveReasoningTransport identifies openai-compatible-responses and custom responses variants as opaque transport", () => {
|
||||
assert.equal(resolveReasoningTransport("openai-compatible-responses-codex"), "opaque");
|
||||
assert.equal(resolveReasoningTransport("custom-openai-responses"), "opaque");
|
||||
assert.equal(resolveReasoningTransport("codex-proxy"), "opaque");
|
||||
assert.equal(resolveReasoningTransport("openai"), "opaque");
|
||||
assert.equal(resolveReasoningTransport("codex"), "opaque");
|
||||
});
|
||||
|
||||
test("#12128: translateRequest to Responses target strips plaintext reasoning.content for opaque responses providers", () => {
|
||||
const chatBody = {
|
||||
model: "gpt-5.6-codex",
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Hi there!",
|
||||
reasoning_content: "Let me think about how to greet the user properly.",
|
||||
},
|
||||
{ role: "user", content: "what is 2+2?" },
|
||||
],
|
||||
};
|
||||
|
||||
const translated = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
"gpt-5.6-codex",
|
||||
chatBody,
|
||||
false,
|
||||
null,
|
||||
"openai-compatible-responses-codex"
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.ok(Array.isArray(translated.input), "translated.input must be an array");
|
||||
|
||||
const input = translated.input as Record<string, unknown>[];
|
||||
const reasoningItems = input.filter((item) => item.type === "reasoning");
|
||||
|
||||
// For opaque responses targets without encrypted continuation, orphaned plaintext reasoning items
|
||||
// must either be stripped entirely or have content.length === 0, so strict Codex backends do not 400.
|
||||
for (const r of reasoningItems) {
|
||||
assert.ok(
|
||||
!r.content || (Array.isArray(r.content) && r.content.length === 0),
|
||||
"reasoning item must not carry non-empty content array to strict responses endpoints"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("#12128: applyReasoningInputPolicy directly sanitizes replayed plaintext reasoning for openai-compatible-responses", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
input: [
|
||||
{
|
||||
type: "reasoning",
|
||||
content: [{ type: "reasoning_text", text: "step 1 plan" }],
|
||||
summary: [],
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "Done" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
applyReasoningInputPolicy(body, "responses", {
|
||||
provider: "openai-compatible-responses-vllm",
|
||||
onIncompatibleReasoning: "drop",
|
||||
});
|
||||
|
||||
const input = body.input as Record<string, unknown>[];
|
||||
const reasoningItems = input.filter((item) => item.type === "reasoning");
|
||||
|
||||
for (const r of reasoningItems) {
|
||||
assert.equal(
|
||||
r.content,
|
||||
undefined,
|
||||
"plaintext reasoning content must be dropped for opaque responses providers"
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user