fix: gate openai reasoning downgrade on model switches (#1562) (thanks @roshanasingh4)

This commit is contained in:
Peter Steinberger
2026-01-24 07:58:04 +00:00
parent 3fff943ba1
commit c97bf23a4a
6 changed files with 239 additions and 41 deletions

View File

@@ -6,20 +6,45 @@ type OpenAIThinkingBlock = {
thinkingSignature?: unknown;
};
function isOrphanedOpenAIReasoningSignature(signature: string): boolean {
const trimmed = signature.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
try {
const parsed = JSON.parse(trimmed) as { id?: unknown; type?: unknown };
const id = typeof parsed?.id === "string" ? parsed.id : "";
const type = typeof parsed?.type === "string" ? parsed.type : "";
if (!id.startsWith("rs_")) return false;
if (type === "reasoning") return true;
if (type.startsWith("reasoning.")) return true;
return false;
} catch {
return false;
type OpenAIReasoningSignature = {
id: string;
type: string;
};
function parseOpenAIReasoningSignature(value: unknown): OpenAIReasoningSignature | null {
if (!value) return null;
let candidate: { id?: unknown; type?: unknown } | null = null;
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
try {
candidate = JSON.parse(trimmed) as { id?: unknown; type?: unknown };
} catch {
return null;
}
} else if (typeof value === "object") {
candidate = value as { id?: unknown; type?: unknown };
}
if (!candidate) return null;
const id = typeof candidate.id === "string" ? candidate.id : "";
const type = typeof candidate.type === "string" ? candidate.type : "";
if (!id.startsWith("rs_")) return null;
if (type === "reasoning" || type.startsWith("reasoning.")) {
return { id, type };
}
return null;
}
function hasFollowingNonThinkingBlock(
content: Extract<AgentMessage, { role: "assistant" }>["content"],
index: number,
): boolean {
for (let i = index + 1; i < content.length; i++) {
const block = content[i];
if (!block || typeof block !== "object") return true;
if ((block as { type?: unknown }).type !== "thinking") return true;
}
return false;
}
/**
@@ -27,7 +52,7 @@ function isOrphanedOpenAIReasoningSignature(signature: string): boolean {
* without the required following item.
*
* Clawdbot persists provider-specific reasoning metadata in `thinkingSignature`; if that metadata
* is incomplete, we downgrade the block to plain text (or drop it if empty) to keep history usable.
* is incomplete, drop the block to keep history usable.
*/
export function downgradeOpenAIReasoningBlocks(messages: AgentMessage[]): AgentMessage[] {
const out: AgentMessage[] = [];
@@ -53,23 +78,29 @@ export function downgradeOpenAIReasoningBlocks(messages: AgentMessage[]): AgentM
let changed = false;
type AssistantContentBlock = (typeof assistantMsg.content)[number];
const nextContent = assistantMsg.content.flatMap((block): AssistantContentBlock[] => {
if (!block || typeof block !== "object") return [block as AssistantContentBlock];
const record = block as OpenAIThinkingBlock;
if (record.type !== "thinking") return [block as AssistantContentBlock];
const signature = typeof record.thinkingSignature === "string" ? record.thinkingSignature : "";
if (!signature || !isOrphanedOpenAIReasoningSignature(signature)) {
return [block as AssistantContentBlock];
const nextContent: AssistantContentBlock[] = [];
for (let i = 0; i < assistantMsg.content.length; i++) {
const block = assistantMsg.content[i];
if (!block || typeof block !== "object") {
nextContent.push(block as AssistantContentBlock);
continue;
}
const record = block as OpenAIThinkingBlock;
if (record.type !== "thinking") {
nextContent.push(block as AssistantContentBlock);
continue;
}
const signature = parseOpenAIReasoningSignature(record.thinkingSignature);
if (!signature) {
nextContent.push(block as AssistantContentBlock);
continue;
}
if (hasFollowingNonThinkingBlock(assistantMsg.content, i)) {
nextContent.push(block as AssistantContentBlock);
continue;
}
const thinking = typeof record.thinking === "string" ? record.thinking : "";
const trimmed = thinking.trim();
changed = true;
if (!trimmed) return [];
return [{ type: "text" as const, text: thinking }];
});
}
if (!changed) {
out.push(msg);