fix(api): drop orphan tool results to satisfy strict role sequence (#745)

* fix(api): drop orphan tool results to satisfy Mistral/OpenAI strict role sequence

* test: add test for orphan tool results and restore gemini comments
This commit is contained in:
3kin0x
2026-04-19 02:57:14 +02:00
committed by GitHub
parent 55c5f262a9
commit b786b765f0
2 changed files with 107 additions and 9 deletions

View File

@@ -349,6 +349,7 @@ function convertMessages(
system: unknown,
): OpenAIMessage[] {
const result: OpenAIMessage[] = []
const knownToolCallIds = new Set<string>()
// System message first
const sysText = convertSystemPrompt(system)
@@ -368,13 +369,21 @@ function convertMessages(
const toolResults = content.filter((b: { type?: string }) => b.type === 'tool_result')
const otherContent = content.filter((b: { type?: string }) => b.type !== 'tool_result')
// Emit tool results as tool messages
// Emit tool results as tool messages, but ONLY if we have a matching tool_use ID.
// Mistral/OpenAI strictly require tool messages to follow an assistant message with tool_calls.
// If the user interrupted (ESC) and a synthetic tool_result was generated without a recorded tool_use,
// emitting it here would cause a "role must alternate" or "unexpected role" error.
for (const tr of toolResults) {
result.push({
role: 'tool',
tool_call_id: tr.tool_use_id ?? 'unknown',
content: convertToolResultContent(tr.content, tr.is_error),
})
const id = tr.tool_use_id ?? 'unknown'
if (knownToolCallIds.has(id)) {
result.push({
role: 'tool',
tool_call_id: id,
content: convertToolResultContent(tr.content, tr.is_error),
})
} else {
logForDebugging(`Dropping orphan tool_result for ID: ${id} to prevent API error`)
}
}
// Emit remaining user content
@@ -415,9 +424,11 @@ function convertMessages(
input?: unknown
extra_content?: Record<string, unknown>
signature?: string
}, index) => {
}) => {
const id = tu.id ?? `call_${crypto.randomUUID().replace(/-/g, '')}`
knownToolCallIds.add(id)
const toolCall: NonNullable<OpenAIMessage['tool_calls']>[number] = {
id: tu.id ?? `call_${crypto.randomUUID().replace(/-/g, '')}`,
id,
type: 'function' as const,
function: {
name: tu.name ?? 'unknown',
@@ -442,7 +453,6 @@ function convertMessages(
// Merge into existing google-specific metadata if present
const existingGoogle = (toolCall.extra_content?.google as Record<string, unknown>) ?? {}
toolCall.extra_content = {
...toolCall.extra_content,
google: {