{"record":{"id":"dabb1352801040c1","repo":"can1357/oh-my-pi","slug":"received-thinking-end-for-non-thinking-content","errorCode":null,"errorMessage":"Received thinking_end for non-thinking content","messagePattern":"Received thinking_end for non-thinking content","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/proxy.ts","lineNumber":319,"sourceCode":"\t\t\t\t\tdelta: proxyEvent.delta,\n\t\t\t\t\tpartial,\n\t\t\t\t};\n\t\t\t}\n\t\t\tthrow new Error(\"Received thinking_delta for non-thinking content\");\n\t\t}\n\n\t\tcase \"thinking_end\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"thinking\") {\n\t\t\t\tcontent.thinkingSignature = proxyEvent.contentSignature;\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"thinking_end\",\n\t\t\t\t\tcontentIndex: proxyEvent.contentIndex,\n\t\t\t\t\tcontent: content.thinking,\n\t\t\t\t\tpartial,\n\t\t\t\t};\n\t\t\t}\n\t\t\tthrow new Error(\"Received thinking_end for non-thinking content\");\n\t\t}\n\n\t\tcase \"image_end\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = proxyEvent.content;\n\t\t\treturn {\n\t\t\t\ttype: \"image_end\",\n\t\t\t\tcontentIndex: proxyEvent.contentIndex,\n\t\t\t\tcontent: proxyEvent.content,\n\t\t\t\tpartial,\n\t\t\t};\n\n\t\tcase \"toolcall_start\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = {\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\tid: proxyEvent.id,\n\t\t\t\tname: proxyEvent.toolName,\n\t\t\t\targuments: {},\n\t\t\t\t[kStreamingPartialJson]: \"\",","sourceCodeStart":301,"sourceCodeEnd":337,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/agent/src/proxy.ts#L301-L337","documentation":"thinking_end attaches the content signature to the thinking block at contentIndex; the slot must already be a thinking block. A missing or mismatched slot means the event stream is inconsistent with the partially built AssistantMessage, so the library throws rather than writing a signature onto the wrong block.","triggerScenarios":"thinking_end for an index that never received thinking_start; the block at the index was replaced by text/toolCall/image; duplicate or out-of-order thinking_end events.","commonSituations":"Custom providers whose reasoning stream ends on a shifted index; event replay where the start was pruned; providers that reuse indexes across block types in one turn.","solutions":["Confirm the adapter closes reasoning on the same contentIndex it opened","Inspect partial.content at failure time to find the actual block type occupying the index","Avoid sharing one contentIndex between thinking and text blocks","Align package versions between proxy producer and agent consumer"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"if (partial.content[ev.contentIndex]?.type !== \"thinking\") {\n  throw new Error(`thinking_end at ${ev.contentIndex} without a thinking block`);\n}","typeGuard":"function canCloseThinking(partial: AssistantMessage, i: number): boolean {\n  return partial.content[i]?.type === \"thinking\";\n}","tryCatchPattern":"try {\n  processProxyEvent(model, ev, partial, partialJsonByIndex);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"thinking_end for non-thinking\")) {\n    logger.warn(\"Ignoring orphan thinking_end\", { contentIndex: ev.contentIndex });\n    return;\n  }\n  throw err;\n}","preventionTips":["Close thinking on the same contentIndex that opened it","Emit at most one thinking_end per thinking_start","Keep stream state per-request; reset fully on retry","Integration-test providers that emit signatures with reasoning blocks"],"tags":["streaming","protocol","thinking"],"backgroundTag":"stream-out-of-order","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}