diff --git a/packages/ai/src/openai-completions-messages.test.ts b/packages/ai/src/openai-completions-messages.test.ts new file mode 100644 index 000000000000..42e552d7c7ce --- /dev/null +++ b/packages/ai/src/openai-completions-messages.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { convertMessages } from "./openai-completions-messages.js"; +import { resolveOpenAICompletionsCompat } from "./transports/openai-completions-compat.js"; +import type { AssistantMessage, Context, Model } from "./types.js"; + +const model: Model<"openai-completions"> = { + id: "test-model", + name: "Test model", + api: "openai-completions", + provider: "custom-openai-compatible", + baseUrl: "https://proxy.example/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 4_096, +}; + +const emptyUsage = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, +}; + +describe("convertMessages assistant text replay", () => { + it("keeps separate assistant text blocks apart", () => { + const assistant: AssistantMessage = { + role: "assistant", + api: model.api, + provider: model.provider, + model: model.id, + content: [ + { type: "text", text: "Let me check the file." }, + { type: "text", text: "The file contains X." }, + ], + usage: emptyUsage, + stopReason: "stop", + timestamp: 2, + }; + const context: Context = { + messages: [{ role: "user", content: "hello", timestamp: 1 }, assistant], + }; + + const converted = convertMessages(model, context, resolveOpenAICompletionsCompat(model)); + + const replayed = converted.find((message) => message.role === "assistant"); + expect(replayed?.content).toBe("Let me check the file.\nThe file contains X."); + }); +}); diff --git a/packages/ai/src/openai-completions-messages.ts b/packages/ai/src/openai-completions-messages.ts index 3aa348a55f5e..5a693a4278ce 100644 --- a/packages/ai/src/openai-completions-messages.ts +++ b/packages/ai/src/openai-completions-messages.ts @@ -141,7 +141,9 @@ export function convertMessages( text: sanitizeSurrogates(block.text), }) satisfies ChatCompletionContentPartText, ); - const assistantText = assistantTextParts.map((part) => part.text).join(""); + // Separate content blocks are distinct utterances, so replay them the way + // the string-content flattener does rather than running them together. + const assistantText = assistantTextParts.map((part) => part.text).join("\n"); const nonEmptyThinkingBlocks = msg.content .filter(isThinkingContentBlock)