mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 17:14:57 +00:00
fix(agents): unblock gpt-5.3-codex API-key routing and replay (#31083)
* fix(agents): unblock gpt-5.3-codex API-key replay path * fix(agents): scope OpenAI replay ID rewrites per turn * test: fix nodes-tool mock typing and reformat telegram accounts
This commit is contained in:
@@ -6,6 +6,11 @@ type OpenAIThinkingBlock = {
|
||||
thinkingSignature?: unknown;
|
||||
};
|
||||
|
||||
type OpenAIToolCallBlock = {
|
||||
type?: unknown;
|
||||
id?: unknown;
|
||||
};
|
||||
|
||||
type OpenAIReasoningSignature = {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -59,6 +64,141 @@ function hasFollowingNonThinkingBlock(
|
||||
return false;
|
||||
}
|
||||
|
||||
function splitOpenAIFunctionCallPairing(id: string): {
|
||||
callId: string;
|
||||
itemId?: string;
|
||||
} {
|
||||
const separator = id.indexOf("|");
|
||||
if (separator <= 0 || separator >= id.length - 1) {
|
||||
return { callId: id };
|
||||
}
|
||||
return {
|
||||
callId: id.slice(0, separator),
|
||||
itemId: id.slice(separator + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function isOpenAIToolCallType(type: unknown): boolean {
|
||||
return type === "toolCall" || type === "toolUse" || type === "functionCall";
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI can reject replayed `function_call` items with an `fc_*` id if the
|
||||
* matching `reasoning` item is absent in the same assistant turn.
|
||||
*
|
||||
* When that pairing is missing, strip the `|fc_*` suffix from tool call ids so
|
||||
* pi-ai omits `function_call.id` on replay.
|
||||
*/
|
||||
export function downgradeOpenAIFunctionCallReasoningPairs(
|
||||
messages: AgentMessage[],
|
||||
): AgentMessage[] {
|
||||
let changed = false;
|
||||
const rewrittenMessages: AgentMessage[] = [];
|
||||
let pendingRewrittenIds: Map<string, string> | null = null;
|
||||
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
pendingRewrittenIds = null;
|
||||
rewrittenMessages.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const role = (msg as { role?: unknown }).role;
|
||||
if (role === "assistant") {
|
||||
const assistantMsg = msg as Extract<AgentMessage, { role: "assistant" }>;
|
||||
if (!Array.isArray(assistantMsg.content)) {
|
||||
pendingRewrittenIds = null;
|
||||
rewrittenMessages.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const localRewrittenIds = new Map<string, string>();
|
||||
let seenReplayableReasoning = false;
|
||||
let assistantChanged = false;
|
||||
const nextContent = assistantMsg.content.map((block) => {
|
||||
if (!block || typeof block !== "object") {
|
||||
return block;
|
||||
}
|
||||
|
||||
const thinkingBlock = block as OpenAIThinkingBlock;
|
||||
if (
|
||||
thinkingBlock.type === "thinking" &&
|
||||
parseOpenAIReasoningSignature(thinkingBlock.thinkingSignature)
|
||||
) {
|
||||
seenReplayableReasoning = true;
|
||||
return block;
|
||||
}
|
||||
|
||||
const toolCallBlock = block as OpenAIToolCallBlock;
|
||||
if (!isOpenAIToolCallType(toolCallBlock.type) || typeof toolCallBlock.id !== "string") {
|
||||
return block;
|
||||
}
|
||||
|
||||
const pairing = splitOpenAIFunctionCallPairing(toolCallBlock.id);
|
||||
if (seenReplayableReasoning || !pairing.itemId || !pairing.itemId.startsWith("fc_")) {
|
||||
return block;
|
||||
}
|
||||
|
||||
assistantChanged = true;
|
||||
localRewrittenIds.set(toolCallBlock.id, pairing.callId);
|
||||
return {
|
||||
...(block as unknown as Record<string, unknown>),
|
||||
id: pairing.callId,
|
||||
} as typeof block;
|
||||
});
|
||||
|
||||
pendingRewrittenIds = localRewrittenIds.size > 0 ? localRewrittenIds : null;
|
||||
if (!assistantChanged) {
|
||||
rewrittenMessages.push(msg);
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
rewrittenMessages.push({ ...assistantMsg, content: nextContent } as AgentMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "toolResult" && pendingRewrittenIds && pendingRewrittenIds.size > 0) {
|
||||
const toolResult = msg as Extract<AgentMessage, { role: "toolResult" }> & {
|
||||
toolUseId?: unknown;
|
||||
};
|
||||
let toolResultChanged = false;
|
||||
const updates: Record<string, string> = {};
|
||||
|
||||
if (typeof toolResult.toolCallId === "string") {
|
||||
const nextToolCallId = pendingRewrittenIds.get(toolResult.toolCallId);
|
||||
if (nextToolCallId && nextToolCallId !== toolResult.toolCallId) {
|
||||
updates.toolCallId = nextToolCallId;
|
||||
toolResultChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof toolResult.toolUseId === "string") {
|
||||
const nextToolUseId = pendingRewrittenIds.get(toolResult.toolUseId);
|
||||
if (nextToolUseId && nextToolUseId !== toolResult.toolUseId) {
|
||||
updates.toolUseId = nextToolUseId;
|
||||
toolResultChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!toolResultChanged) {
|
||||
rewrittenMessages.push(msg);
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
rewrittenMessages.push({
|
||||
...toolResult,
|
||||
...updates,
|
||||
} as AgentMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
pendingRewrittenIds = null;
|
||||
rewrittenMessages.push(msg);
|
||||
}
|
||||
|
||||
return changed ? rewrittenMessages : messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Responses API can reject transcripts that contain a standalone `reasoning` item id
|
||||
* without the required following item.
|
||||
|
||||
Reference in New Issue
Block a user