From 445dfffc4a7bb21cae0ef796c5c461235a39b63f Mon Sep 17 00:00:00 2001 From: Seth Raphael Date: Thu, 15 Jan 2026 20:41:32 -0800 Subject: [PATCH 1/6] Fix tool calls being overwritten during streaming The combineUIMessages function was using splice(-1, 1) when previousPartIndex was -1 (tool call not found), which incorrectly removed the last element from the array due to JavaScript's negative indexing behavior. Now explicitly checks for -1 before splicing, ensuring new tool calls are properly added without removing existing ones. Fixes #182 Co-Authored-By: Claude Opus 4.5 --- src/UIMessages.combineUIMessages.test.ts | 239 +++++++++++++++++++++++ src/UIMessages.ts | 5 +- 2 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 src/UIMessages.combineUIMessages.test.ts diff --git a/src/UIMessages.combineUIMessages.test.ts b/src/UIMessages.combineUIMessages.test.ts new file mode 100644 index 00000000..f696de58 --- /dev/null +++ b/src/UIMessages.combineUIMessages.test.ts @@ -0,0 +1,239 @@ +import { describe, it, expect } from "vitest"; +import { combineUIMessages, type UIMessage } from "./UIMessages.js"; + +describe("combineUIMessages", () => { + it("should preserve all tool calls when combining messages", () => { + const messages: UIMessage[] = [ + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "streaming", + role: "assistant", + parts: [ + { + type: "tool-toolA", + toolCallId: "call_A", + state: "input-available", + input: {}, + }, + ], + text: "", + _creationTime: Date.now(), + }, + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "streaming", + role: "assistant", + parts: [ + { + type: "tool-toolB", + toolCallId: "call_B", + state: "input-available", + input: {}, + }, + ], + text: "", + _creationTime: Date.now(), + }, + ]; + + const result = combineUIMessages(messages); + + expect(result).toHaveLength(1); + expect(result[0].parts).toHaveLength(2); + + const toolCallIds = result[0].parts + .filter((p) => p.type.startsWith("tool-")) + .map((p: any) => p.toolCallId); + + expect(toolCallIds).toContain("call_A"); + expect(toolCallIds).toContain("call_B"); + }); + + it("should accumulate tool calls progressively (issue #182)", () => { + // Simulating: A(started) → B → C → A(result) + const messages: UIMessage[] = [ + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "streaming", + role: "assistant", + parts: [ + { + type: "tool-toolA", + toolCallId: "call_A", + state: "input-available", + input: {}, + }, + ], + text: "", + _creationTime: Date.now(), + }, + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "streaming", + role: "assistant", + parts: [ + { + type: "tool-toolA", + toolCallId: "call_A", + state: "input-available", + input: {}, + }, + { + type: "tool-toolB", + toolCallId: "call_B", + state: "input-available", + input: {}, + }, + ], + text: "", + _creationTime: Date.now(), + }, + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "streaming", + role: "assistant", + parts: [ + { + type: "tool-toolA", + toolCallId: "call_A", + state: "input-available", + input: {}, + }, + { + type: "tool-toolB", + toolCallId: "call_B", + state: "input-available", + input: {}, + }, + { + type: "tool-toolC", + toolCallId: "call_C", + state: "input-available", + input: {}, + }, + ], + text: "", + _creationTime: Date.now(), + }, + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "success", + role: "assistant", + parts: [ + { + type: "tool-toolA", + toolCallId: "call_A", + state: "output-available", + input: {}, + output: "success", + }, + { + type: "tool-toolB", + toolCallId: "call_B", + state: "input-available", + input: {}, + }, + { + type: "tool-toolC", + toolCallId: "call_C", + state: "input-available", + input: {}, + }, + ], + text: "", + _creationTime: Date.now(), + }, + ]; + + const result = combineUIMessages(messages); + + expect(result).toHaveLength(1); + expect(result[0].parts).toHaveLength(3); + + const toolCallIds = result[0].parts + .filter((p) => p.type.startsWith("tool-")) + .map((p: any) => p.toolCallId); + + // All tool calls should be present + expect(toolCallIds).toContain("call_A"); + expect(toolCallIds).toContain("call_B"); + expect(toolCallIds).toContain("call_C"); + + // Tool A should have the final state (output-available) + const toolA = result[0].parts.find( + (p: any) => p.type === "tool-toolA" && p.toolCallId === "call_A", + ) as any; + expect(toolA.state).toBe("output-available"); + expect(toolA.output).toBe("success"); + }); + + it("should merge tool calls with same toolCallId", () => { + const messages: UIMessage[] = [ + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "streaming", + role: "assistant", + parts: [ + { + type: "tool-toolA", + toolCallId: "call_A", + state: "input-available", + input: { test: "input" }, + }, + ], + text: "", + _creationTime: Date.now(), + }, + { + id: "msg1", + key: "thread-1-0", + order: 1, + stepOrder: 0, + status: "success", + role: "assistant", + parts: [ + { + type: "tool-toolA", + toolCallId: "call_A", + state: "output-available", + input: { test: "input" }, + output: "completed", + }, + ], + text: "", + _creationTime: Date.now(), + }, + ]; + + const result = combineUIMessages(messages); + + expect(result).toHaveLength(1); + expect(result[0].parts).toHaveLength(1); + + const toolCall = result[0].parts[0] as any; + expect(toolCall.toolCallId).toBe("call_A"); + expect(toolCall.state).toBe("output-available"); + expect(toolCall.output).toBe("completed"); + }); +}); diff --git a/src/UIMessages.ts b/src/UIMessages.ts index 79da9e76..3deec59e 100644 --- a/src/UIMessages.ts +++ b/src/UIMessages.ts @@ -583,11 +583,12 @@ export function combineUIMessages(messages: UIMessage[]): UIMessage[] { const previousPartIndex = newParts.findIndex( (p) => getToolCallId(p) === toolCallId, ); - const previousPart = newParts.splice(previousPartIndex, 1)[0]; - if (!previousPart) { + if (previousPartIndex === -1) { + // Tool call not found in previous parts, add it as new newParts.push(part); continue; } + const previousPart = newParts.splice(previousPartIndex, 1)[0]; newParts.push(mergeParts(previousPart, part)); } acc.push({ From 1752b7ebd39ff4f6d452d6c28c18682f7fcdab17 Mon Sep 17 00:00:00 2001 From: Seth Raphael Date: Thu, 15 Jan 2026 20:49:40 -0800 Subject: [PATCH 2/6] Fix tools not reporting proper errors (output-error state) Two fixes: 1. normalizeToolResult in mapping.ts was stripping the isError flag. Now preserves isError when transforming tool result parts. 2. createAssistantUIMessage in UIMessages.ts now checks contentPart.isError in addition to message.error when determining tool state. This ensures tool results with isError: true show "output-error" state instead of incorrectly showing "output-available". Fixes #162 Co-Authored-By: Claude Opus 4.5 --- src/UIMessages.ts | 14 ++++-- src/mapping.ts | 2 + src/toUIMessages.test.ts | 102 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/src/UIMessages.ts b/src/UIMessages.ts index 79da9e76..56036001 100644 --- a/src/UIMessages.ts +++ b/src/UIMessages.ts @@ -463,6 +463,12 @@ function createAssistantUIMessage< typeof contentPart.output?.type === "string" ? contentPart.output.value : contentPart.output; + // Check for error at both the content part level (isError) and message level + // isError may exist on stored tool results but isn't in ToolResultPart type + const hasError = + (contentPart as { isError?: boolean }).isError || message.error; + const errorText = + message.error || (hasError ? String(output) : undefined); const call = allParts.find( (part) => part.type === `tool-${contentPart.toolName}` && @@ -470,9 +476,9 @@ function createAssistantUIMessage< part.toolCallId === contentPart.toolCallId, ) as ToolUIPart | undefined; if (call) { - if (message.error) { + if (hasError) { call.state = "output-error"; - call.errorText = message.error; + call.errorText = errorText ?? "Unknown error"; call.output = output; } else { call.state = "output-available"; @@ -483,13 +489,13 @@ function createAssistantUIMessage< "Tool result without preceding tool call.. adding anyways", contentPart, ); - if (message.error) { + if (hasError) { allParts.push({ type: `tool-${contentPart.toolName}`, toolCallId: contentPart.toolCallId, state: "output-error", input: undefined, - errorText: message.error, + errorText: errorText ?? "Unknown error", callProviderMetadata: message.providerMetadata, } satisfies ToolUIPart); } else { diff --git a/src/mapping.ts b/src/mapping.ts index 86253c31..ae956ec8 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -549,6 +549,8 @@ function normalizeToolResult( normalizeToolOutput("result" in part ? part.result : undefined), toolCallId: part.toolCallId, toolName: part.toolName, + // Preserve isError flag for error reporting + ...("isError" in part && part.isError ? { isError: true } : {}), ...metadata, } satisfies ToolResultPart; } diff --git a/src/toUIMessages.test.ts b/src/toUIMessages.test.ts index c5aea2a8..3c2bab98 100644 --- a/src/toUIMessages.test.ts +++ b/src/toUIMessages.test.ts @@ -728,4 +728,106 @@ describe("toUIMessages", () => { expect(textParts).toHaveLength(1); expect(textParts[0].text).toBe("The result is 42."); }); + + it("shows output-error state when tool result has isError: true (issue #162)", () => { + const messages = [ + // Tool call + baseMessageDoc({ + _id: "msg1", + order: 1, + stepOrder: 1, + tool: true, + message: { + role: "assistant", + content: [ + { + type: "tool-call", + toolName: "generateImage", + toolCallId: "call1", + args: { id: "invalid-id" }, + }, + ], + }, + }), + // Tool result with error + baseMessageDoc({ + _id: "msg2", + order: 1, + stepOrder: 2, + tool: true, + message: { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call1", + toolName: "generateImage", + output: { + type: "text", + value: + 'ArgumentValidationError: Value does not match validator.\nPath: .id\nValue: "invalid-id"\nValidator: v.id("images")', + }, + isError: true, + }, + ], + }, + }), + ]; + + const uiMessages = toUIMessages(messages); + + expect(uiMessages).toHaveLength(1); + expect(uiMessages[0].role).toBe("assistant"); + + const toolParts = uiMessages[0].parts.filter( + (p) => p.type === "tool-generateImage", + ); + expect(toolParts).toHaveLength(1); + + const toolPart = toolParts[0] as any; + expect(toolPart.toolCallId).toBe("call1"); + // Should show output-error, not output-available + expect(toolPart.state).toBe("output-error"); + expect(toolPart.output).toContain("ArgumentValidationError"); + }); + + it("shows output-error when tool result has isError: true without tool call present (issue #162)", () => { + // This simulates the case where the tool-call message wasn't saved + const messages = [ + baseMessageDoc({ + _id: "msg1", + order: 1, + stepOrder: 1, + tool: true, + message: { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call1", + toolName: "generateImage", + output: { + type: "text", + value: "Error: Something went wrong", + }, + isError: true, + }, + ], + }, + }), + ]; + + const uiMessages = toUIMessages(messages); + + expect(uiMessages).toHaveLength(1); + expect(uiMessages[0].role).toBe("assistant"); + + const toolParts = uiMessages[0].parts.filter( + (p) => p.type === "tool-generateImage", + ); + expect(toolParts).toHaveLength(1); + + const toolPart = toolParts[0] as any; + expect(toolPart.state).toBe("output-error"); + }); }); From ab6a8b0e8037e7344e8f61ed262e156c44ecc27f Mon Sep 17 00:00:00 2001 From: Seth Raphael Date: Thu, 15 Jan 2026 22:03:19 -0800 Subject: [PATCH 3/6] Fix #188: Support userId in UIMessage for multi-user threads When converting MessageDoc to UIMessage via toUIMessages(), the userId field was being lost. This made it difficult to identify which user sent each message in multi-user thread conversations. Changes: - Add userId?: string to UIMessage type definition - Include userId in createSystemUIMessage() - Include userId in createUserUIMessage() - Include userId in createAssistantUIMessage() (from first message) - Add tests for userId preservation across all message types Co-Authored-By: Claude Opus 4.5 --- src/UIMessages.ts | 7 ++- src/toUIMessages.test.ts | 127 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/UIMessages.ts b/src/UIMessages.ts index 79da9e76..d5f4a43d 100644 --- a/src/UIMessages.ts +++ b/src/UIMessages.ts @@ -42,6 +42,7 @@ export type UIMessage< stepOrder: number; status: UIStatus; agentName?: string; + userId?: string; text: string; _creationTime: number; }; @@ -75,7 +76,8 @@ export function fromUIMessages( "providerOptions", "metadata", ]), - ...omit(uiMessage, ["parts", "role", "key", "text"]), + ...omit(uiMessage, ["parts", "role", "key", "text", "userId"]), + userId: uiMessage.userId ?? meta.userId, status: uiMessage.status === "streaming" ? "pending" : "success", streaming: uiMessage.status === "streaming", // to override @@ -288,6 +290,7 @@ function createSystemUIMessage< text, role: "system", agentName: message.agentName, + userId: message.userId, parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart], metadata: message.metadata, }; @@ -347,6 +350,7 @@ function createUserUIMessage< key: `${message.threadId}-${message.order}-${message.stepOrder}`, text, role: "user", + userId: message.userId, parts, metadata: message.metadata, }; @@ -370,6 +374,7 @@ function createAssistantUIMessage< stepOrder: firstMessage.stepOrder, key: `${firstMessage.threadId}-${firstMessage.order}-${firstMessage.stepOrder}`, agentName: firstMessage.agentName, + userId: firstMessage.userId, }; // Get status from last message diff --git a/src/toUIMessages.test.ts b/src/toUIMessages.test.ts index c5aea2a8..58bcaacb 100644 --- a/src/toUIMessages.test.ts +++ b/src/toUIMessages.test.ts @@ -728,4 +728,131 @@ describe("toUIMessages", () => { expect(textParts).toHaveLength(1); expect(textParts[0].text).toBe("The result is 42."); }); + + describe("userId preservation", () => { + it("preserves userId in user messages", () => { + const messages = [ + baseMessageDoc({ + userId: "user123", + message: { + role: "user", + content: "Hello!", + }, + text: "Hello!", + }), + ]; + const uiMessages = toUIMessages(messages); + expect(uiMessages).toHaveLength(1); + expect(uiMessages[0].role).toBe("user"); + expect(uiMessages[0].userId).toBe("user123"); + }); + + it("preserves userId in system messages", () => { + const messages = [ + baseMessageDoc({ + userId: "user456", + message: { + role: "system", + content: "System prompt", + }, + text: "System prompt", + }), + ]; + const uiMessages = toUIMessages(messages); + expect(uiMessages).toHaveLength(1); + expect(uiMessages[0].role).toBe("system"); + expect(uiMessages[0].userId).toBe("user456"); + }); + + it("preserves userId in assistant messages", () => { + const messages = [ + baseMessageDoc({ + userId: "user789", + message: { + role: "assistant", + content: "Hi there!", + }, + text: "Hi there!", + }), + ]; + const uiMessages = toUIMessages(messages); + expect(uiMessages).toHaveLength(1); + expect(uiMessages[0].role).toBe("assistant"); + expect(uiMessages[0].userId).toBe("user789"); + }); + + it("preserves userId from first message in grouped assistant messages", () => { + const messages = [ + baseMessageDoc({ + _id: "msg1", + userId: "userA", + order: 1, + stepOrder: 1, + tool: true, + message: { + role: "assistant", + content: [ + { + type: "tool-call", + toolName: "myTool", + toolCallId: "call1", + args: {}, + }, + ], + }, + text: "", + }), + baseMessageDoc({ + _id: "msg2", + userId: "userA", + order: 1, + stepOrder: 2, + tool: true, + message: { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call1", + toolName: "myTool", + output: { type: "text", value: "result" }, + }, + ], + }, + text: "", + }), + baseMessageDoc({ + _id: "msg3", + userId: "userA", + order: 1, + stepOrder: 3, + message: { + role: "assistant", + content: "Done!", + }, + text: "Done!", + }), + ]; + const uiMessages = toUIMessages(messages); + expect(uiMessages).toHaveLength(1); + expect(uiMessages[0].role).toBe("assistant"); + expect(uiMessages[0].userId).toBe("userA"); + }); + + it("handles undefined userId gracefully", () => { + const messages = [ + baseMessageDoc({ + // No userId provided + message: { + role: "user", + content: "Hello!", + }, + text: "Hello!", + }), + ]; + const uiMessages = toUIMessages(messages); + expect(uiMessages).toHaveLength(1); + expect(uiMessages[0].userId).toBeUndefined(); + }); + }); }); From 10154dc3df94021ec411484e5f7ee429cd77f36b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:10:10 +0000 Subject: [PATCH 4/6] Initial plan From fd76ccfb885e27896522495bce297ed7c7b1801a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:23:27 +0000 Subject: [PATCH 5/6] Fix merge conflicts and restore async fromUIMessages for AI SDK v6 compatibility Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com> --- src/UIMessages.ts | 131 +++++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/src/UIMessages.ts b/src/UIMessages.ts index 41eb2d46..ad8f80e2 100644 --- a/src/UIMessages.ts +++ b/src/UIMessages.ts @@ -8,6 +8,7 @@ import { type SourceUrlUIPart, type StepStartUIPart, type TextUIPart, + type ToolResultPart, type ToolUIPart, type UIDataTypes, type UITools, @@ -54,7 +55,7 @@ export type UIMessage< * @param meta - The metadata to add to the MessageDocs. * @returns */ -export function fromUIMessages( +export async function fromUIMessages( messages: UIMessage[], meta: { threadId: string; @@ -64,59 +65,67 @@ export function fromUIMessages( providerOptions?: ProviderOptions; metadata?: METADATA; }, -): (MessageDoc & { streaming: boolean; metadata?: METADATA })[] { - return messages.flatMap((uiMessage) => { - const stepOrder = uiMessage.stepOrder; - const commonFields = { - ...pick(meta, [ - "threadId", - "userId", - "model", - "provider", - "providerOptions", - "metadata", - ]), - ...omit(uiMessage, ["parts", "role", "key", "text", "userId"]), - userId: uiMessage.userId ?? meta.userId, - status: uiMessage.status === "streaming" ? "pending" : "success", - streaming: uiMessage.status === "streaming", - // to override - _id: uiMessage.id, - tool: false, - } satisfies MessageDoc & { streaming: boolean; metadata?: METADATA }; - const modelMessages = convertToModelMessages([uiMessage]); - return modelMessages - .map((modelMessage, i) => { - if (modelMessage.content.length === 0) { - return undefined; - } - const message = fromModelMessage(modelMessage); - const tool = isTool(message); - const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } = { - ...commonFields, - _id: uiMessage.id + `-${i}`, - stepOrder: stepOrder + i, - message, - tool, - text: extractText(message), - reasoning: extractReasoning(message), - finishReason: tool ? "tool-calls" : "stop", - sources: fromSourceParts(uiMessage.parts), - }; - if (Array.isArray(modelMessage.content)) { - const providerOptions = modelMessage.content.find( - (c) => c.providerOptions, - )?.providerOptions; - if (providerOptions) { - // convertToModelMessages changes providerMetadata to providerOptions - doc.providerMetadata = providerOptions; - doc.providerOptions ??= providerOptions; +): Promise<(MessageDoc & { streaming: boolean; metadata?: METADATA })[]> { + const nested = await Promise.all( + messages.map(async (uiMessage) => { + const stepOrder = uiMessage.stepOrder; + const commonFields = { + ...pick(meta, [ + "threadId", + "userId", + "model", + "provider", + "providerOptions", + "metadata", + ]), + ...omit(uiMessage, ["parts", "role", "key", "text", "userId"]), + userId: uiMessage.userId ?? meta.userId, + status: uiMessage.status === "streaming" ? "pending" : "success", + streaming: uiMessage.status === "streaming", + // to override + _id: uiMessage.id, + tool: false, + } satisfies MessageDoc & { streaming: boolean; metadata?: METADATA }; + const modelMessages = await convertToModelMessages([uiMessage]); + return modelMessages + .map((modelMessage, i) => { + if (modelMessage.content.length === 0) { + return undefined; } - } - return doc; - }) - .filter((d) => d !== undefined); - }); + const message = fromModelMessage(modelMessage); + const tool = isTool(message); + const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } = + { + ...commonFields, + _id: uiMessage.id + `-${i}`, + stepOrder: stepOrder + i, + message, + tool, + text: extractText(message), + reasoning: extractReasoning(message), + finishReason: tool ? "tool-calls" : "stop", + sources: fromSourceParts(uiMessage.parts), + }; + if (Array.isArray(modelMessage.content)) { + const providerOptions = ( + modelMessage.content.find( + (c) => (c as { providerOptions?: unknown }).providerOptions, + ) as { providerOptions?: unknown } | undefined + )?.providerOptions as + | Record> + | undefined; + if (providerOptions) { + // convertToModelMessages changes providerMetadata to providerOptions + doc.providerMetadata = providerOptions; + doc.providerOptions ??= providerOptions; + } + } + return doc; + }) + .filter((d) => d !== undefined); + }), + ); + return nested.flat(); } function fromSourceParts(parts: UIMessage["parts"]): Infer[] { @@ -464,10 +473,13 @@ function createAssistantUIMessage< break; } case "tool-result": { + const typedPart = contentPart as unknown as ToolResultPart & { + output: { type: string; value: unknown }; + }; const output = - typeof contentPart.output?.type === "string" - ? contentPart.output.value - : contentPart.output; + typeof typedPart.output?.type === "string" + ? typedPart.output.value + : typedPart.output; // Check for error at both the content part level (isError) and message level // isError may exist on stored tool results but isn't in ToolResultPart type const hasError = @@ -517,7 +529,7 @@ function createAssistantUIMessage< break; } default: { - const maybeSource = contentPart as SourcePart; + const maybeSource = contentPart as unknown as SourcePart; if (maybeSource.type === "source") { allParts.push(toSourcePart(maybeSource)); } else { @@ -594,11 +606,12 @@ export function combineUIMessages(messages: UIMessage[]): UIMessage[] { const previousPartIndex = newParts.findIndex( (p) => getToolCallId(p) === toolCallId, ); - const previousPart = newParts.splice(previousPartIndex, 1)[0]; - if (!previousPart) { + if (previousPartIndex === -1) { + // Tool call not found in previous parts, add it as new newParts.push(part); continue; } + const previousPart = newParts.splice(previousPartIndex, 1)[0]; newParts.push(mergeParts(previousPart, part)); } acc.push({ From e03ecf6e9df1226ff98987ae5ded76dbf8d16cf6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:26:17 +0000 Subject: [PATCH 6/6] Improve readability of providerOptions type casting Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com> --- src/UIMessages.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/UIMessages.ts b/src/UIMessages.ts index ad8f80e2..d65de779 100644 --- a/src/UIMessages.ts +++ b/src/UIMessages.ts @@ -107,17 +107,20 @@ export async function fromUIMessages( sources: fromSourceParts(uiMessage.parts), }; if (Array.isArray(modelMessage.content)) { - const providerOptions = ( - modelMessage.content.find( - (c) => (c as { providerOptions?: unknown }).providerOptions, - ) as { providerOptions?: unknown } | undefined - )?.providerOptions as - | Record> - | undefined; - if (providerOptions) { + // Find a content part with providerOptions (type assertion needed for SDK compatibility) + const partWithProviderOptions = modelMessage.content.find( + (c): c is typeof c & { providerOptions: unknown } => + "providerOptions" in c && c.providerOptions !== undefined, + ); + if (partWithProviderOptions?.providerOptions) { // convertToModelMessages changes providerMetadata to providerOptions - doc.providerMetadata = providerOptions; - doc.providerOptions ??= providerOptions; + const providerOptions = partWithProviderOptions.providerOptions as + | Record> + | undefined; + if (providerOptions) { + doc.providerMetadata = providerOptions; + doc.providerOptions ??= providerOptions; + } } } return doc;