can1357/oh-my-pi · error

Received toolcall_delta for non-toolCall content

Error message

Received toolcall_delta for non-toolCall content

What it means

toolcall_delta accumulates streamed JSON argument fragments for a toolCall block; the slot at contentIndex must be a toolCall block created by toolcall_start. If the slot is absent or another type, argument chunks cannot be attributed, so the library throws instead of merging JSON into the wrong block.

Source

Thrown at packages/agent/src/proxy.ts:356

			} as ToolCall & StreamingPartialJsonCarrier;
			partialJsonByIndex.set(proxyEvent.contentIndex, "");
			return { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
		case "toolcall_delta": {
			const content = partial.content[proxyEvent.contentIndex];
			if (content?.type === "toolCall") {
				const acc = (partialJsonByIndex.get(proxyEvent.contentIndex) ?? "") + proxyEvent.delta;
				partialJsonByIndex.set(proxyEvent.contentIndex, acc);
				content.arguments = parseStreamingJson(acc) || {};
				setStreamingPartialJson(content, acc);
				partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity
				return {
					type: "toolcall_delta",
					contentIndex: proxyEvent.contentIndex,
					delta: proxyEvent.delta,
					partial,
				};
			}
			throw new Error("Received toolcall_delta for non-toolCall content");
		}

		case "toolcall_end": {
			const content = partial.content[proxyEvent.contentIndex];
			if (content?.type === "toolCall") {
				partialJsonByIndex.delete(proxyEvent.contentIndex);
				clearStreamingPartialJson(content);
				return {
					type: "toolcall_end",
					contentIndex: proxyEvent.contentIndex,
					toolCall: content,
					partial,
				};
			}
			return undefined;
		}

		case "done":

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure toolcall_start precedes every toolcall_delta with the identical contentIndex and that partialJsonByIndex is populated
  2. Fix the adapter's tool-call index mapping (provider tool index -> proxy contentIndex)
  3. Don't overwrite a toolCall slot with another block type while deltas are still streaming
  4. Reset the stream state (including partialJsonByIndex) on retry instead of resuming mid-call

Example fix

// before: delta keyed by provider tool index
yield { type: "toolcall_delta", contentIndex: toolChunk.index, delta: toolChunk.args }
// after: map to the contentIndex used at toolcall_start
yield { type: "toolcall_delta", contentIndex: indexMap.get(toolChunk.index), delta: toolChunk.args }
Defensive patterns

Strategy: validation

Validate before calling

const slot = partial.content[ev.contentIndex];
if (slot?.type !== "toolCall" || !partialJsonByIndex.has(ev.contentIndex)) {
  throw new Error(`toolcall_delta at ${ev.contentIndex} without an open toolCall`);
}

Type guard

function isOpenToolCall(partial: AssistantMessage, i: number, acc: Map<number, string>): boolean {
  return partial.content[i]?.type === "toolCall" && acc.has(i);
}

Try / catch

try {
  processProxyEvent(model, ev, partial, partialJsonByIndex);
} catch (err) {
  if (err instanceof Error && err.message.includes("toolcall_delta for non-toolCall")) {
    logger.warn("Dropping orphan toolcall_delta", { contentIndex: ev.contentIndex });
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: toolcall_delta without a preceding toolcall_start at the same index; the slot was overwritten by text/thinking; adapter indexes tool-call argument chunks differently from the start event; duplicate toolcall_start cleared by a later toolcall_end then deltas keep arriving.

Common situations: OpenAI-style providers that stream tool-call argument deltas with a per-tool index the adapter mis-maps; parallel tool calls where indexes collide; replaying partial streams after reconnect.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a56a8f39c1d2fa0b. Report an issue: GitHub.