mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
fix(translator): drop or map agent_message on Chat Completions fallback (#12880)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados. `agent_message` chegando num fallback de Chat Completions é um item que o cliente não sabe interpretar; mapear ou descartar é a escolha certa, e escolher por item em vez de derrubar a resposta inteira mantém o fallback útil.
This commit is contained in:
@@ -227,7 +227,7 @@ export function openaiResponsesToOpenAIRequest(
|
||||
const itemType = toString(item.type) || (item.role ? "message" : "");
|
||||
|
||||
if (itemType === "message") {
|
||||
const role = toString(item.role);
|
||||
const role = toString(item.role) === "agent_message" ? "assistant" : toString(item.role);
|
||||
|
||||
if (role !== "assistant") {
|
||||
if (currentAssistantMsg) {
|
||||
@@ -484,6 +484,13 @@ export function openaiResponsesToOpenAIRequest(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Defense in depth for Responses/subagent fallback: agent_message is
|
||||
// Responses-only. Normalization should already have rewritten or dropped it;
|
||||
// never throw a 5xx-looking unsupported-feature error if a shape slips through.
|
||||
if (itemType === "agent_message" || toString(item.role) === "agent_message") {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw unsupportedFeature(
|
||||
`Unsupported Responses API feature: input item type '${itemType || "missing"}' cannot be represented in Chat Completions`
|
||||
);
|
||||
@@ -773,7 +780,10 @@ export function openaiResponsesToOpenAIRequest(
|
||||
// ("When using tool_choice, tools must be set"). Contradictory choices like "required"
|
||||
// or forced functions are preserved so the upstream error remains visible.
|
||||
const finalChatTools = Array.isArray(result.tools) ? result.tools : [];
|
||||
if (finalChatTools.length === 0 && (result.tool_choice === "auto" || result.tool_choice === "none")) {
|
||||
if (
|
||||
finalChatTools.length === 0 &&
|
||||
(result.tool_choice === "auto" || result.tool_choice === "none")
|
||||
) {
|
||||
delete result.tool_choice;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function normalizeAgentMessageForChat(item: JsonRecord): JsonRecord | null {
|
||||
if (item.type !== "agent_message") return null;
|
||||
function isAgentMessageItem(item: JsonRecord): boolean {
|
||||
return item.type === "agent_message" || item.role === "agent_message";
|
||||
}
|
||||
|
||||
function collectAgentMessageText(item: JsonRecord): string | null {
|
||||
if (typeof item.content === "string") return item.content;
|
||||
if (typeof item.text === "string") return item.text;
|
||||
if (!Array.isArray(item.content)) return null;
|
||||
|
||||
const textParts: string[] = [];
|
||||
for (const partValue of item.content) {
|
||||
if (typeof partValue === "string") {
|
||||
textParts.push(partValue);
|
||||
continue;
|
||||
}
|
||||
if (!partValue || typeof partValue !== "object" || Array.isArray(partValue)) {
|
||||
return null;
|
||||
}
|
||||
@@ -17,12 +25,21 @@ function normalizeAgentMessageForChat(item: JsonRecord): JsonRecord | null {
|
||||
// partial plaintext envelope or forward an opaque payload the model cannot use.
|
||||
return null;
|
||||
}
|
||||
if (part.type !== "input_text" || typeof part.text !== "string") return null;
|
||||
if (part.type !== "input_text" && part.type !== "output_text" && part.type !== "text") {
|
||||
return null;
|
||||
}
|
||||
if (typeof part.text !== "string") return null;
|
||||
textParts.push(part.text);
|
||||
}
|
||||
|
||||
const text = textParts.join("\n");
|
||||
if (!text.trim()) return null;
|
||||
return textParts.join("\n");
|
||||
}
|
||||
|
||||
function normalizeAgentMessageForChat(item: JsonRecord): JsonRecord | null {
|
||||
if (!isAgentMessageItem(item)) return null;
|
||||
|
||||
const text = collectAgentMessageText(item);
|
||||
if (typeof text !== "string" || !text.trim()) return null;
|
||||
|
||||
return {
|
||||
type: "message",
|
||||
@@ -125,7 +142,7 @@ function normalizeResponsesInputItemForChat(value: unknown): unknown {
|
||||
|
||||
const agentMessage = normalizeAgentMessageForChat(item);
|
||||
if (agentMessage) return agentMessage;
|
||||
if (item.type === "agent_message") {
|
||||
if (isAgentMessageItem(item)) {
|
||||
// Encrypted or malformed agent messages have no lossless Chat equivalent.
|
||||
// Treat them like other Responses-only metadata instead of failing the whole turn.
|
||||
return { type: "reasoning" };
|
||||
|
||||
@@ -178,6 +178,47 @@ test("Responses -> Chat skips encrypted or mixed agent_message items", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Responses -> Chat converts string-content agent_message items to assistant history", () => {
|
||||
const result = translate({
|
||||
input: [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Run the task" }] },
|
||||
{ type: "agent_message", author: "worker", content: "Task completed" },
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.messages, [
|
||||
{ role: "user", content: [{ type: "text", text: "Run the task" }] },
|
||||
{ role: "assistant", content: [{ type: "text", text: "Task completed" }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Responses -> Chat converts role-based agent_message items without a type field", () => {
|
||||
const result = translate({
|
||||
input: [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Run the task" }] },
|
||||
{ role: "agent_message", content: [{ type: "text", text: "Worker reply" }] },
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.messages, [
|
||||
{ role: "user", content: [{ type: "text", text: "Run the task" }] },
|
||||
{ role: "assistant", content: [{ type: "text", text: "Worker reply" }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Responses -> Chat does not throw when an agent_message item slips past normalize", () => {
|
||||
const result = translate({
|
||||
input: [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Run the task" }] },
|
||||
{ type: "agent_message", content: [{ type: "unknown_part" }] },
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.messages, [
|
||||
{ role: "user", content: [{ type: "text", text: "Run the task" }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Responses -> Chat consumes additional_tools input items without emitting messages", () => {
|
||||
const result = translate({
|
||||
input: [
|
||||
|
||||
Reference in New Issue
Block a user