Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion test/harness/anthropicMessagesAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import type { ChatCompletion } from "openai/resources/chat/completions";
import {
canonicalUserMessageSeparator,
CanonicalMessage,
CanonicalToolCall,
formatSseEvent,
Expand Down Expand Up @@ -178,7 +179,7 @@ function convertAnthropicUserMessage(
content: onlyText
? contentParts
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n")
.join(canonicalUserMessageSeparator)
Comment thread
stephentoub marked this conversation as resolved.
: [...contentParts],
});
contentParts.length = 0;
Expand Down
2 changes: 2 additions & 0 deletions test/harness/modelProtocolAdapterShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

export type JsonObject = Record<string, unknown>;

export const canonicalUserMessageSeparator = "\n\n\n";

export type CanonicalToolCall = {
id: string;
type: "function";
Expand Down
76 changes: 76 additions & 0 deletions test/harness/modelProtocolAdapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,52 @@ async function postJson(
}

describe("Anthropic Messages adapter", () => {
test("canonicalizes adjacent plain-text content blocks", () => {
const result = JSON.parse(
anthropicMessagesRequestToChatCompletion(
JSON.stringify({
model: "test-model",
messages: [
{
role: "user",
content: [
{ type: "text", text: "First prompt" },
{ type: "text", text: "Recovery prompt" },
],
},
],
}),
),
) as {
messages: Array<{ role: string; content: unknown }>;
};

expect(result.messages).toEqual([
{
role: "user",
content: "First prompt\n\n\nRecovery prompt",
},
]);
});

test.each([
["string content", "Hello"],
["one text block", [{ type: "text", text: "Hello" }]],
])("preserves a single plain-text user block from %s", (_, content) => {
const result = JSON.parse(
anthropicMessagesRequestToChatCompletion(
JSON.stringify({
model: "test-model",
messages: [{ role: "user", content }],
}),
),
) as {
messages: Array<{ role: string; content: unknown }>;
};

expect(result.messages).toEqual([{ role: "user", content: "Hello" }]);
});

test("normalizes messages, binary content, and tools", () => {
const result = JSON.parse(
anthropicMessagesRequestToChatCompletion(
Expand Down Expand Up @@ -589,6 +635,36 @@ describe("protocol-aware replay", () => {
});
});

test("canonicalizes Anthropic content blocks before coalescing user messages", async () => {
await writeSnapshot([
{ role: "system", content: "${system}" },
{ role: "user", content: "First prompt" },
{ role: "user", content: "Recovery prompt" },
{ role: "user", content: "Final prompt" },
{ role: "assistant", content: "Recovered" },
]);
const request = requestFor("anthropic-messages", "Final prompt");
request.messages = [
{
role: "user",
content: [
{ type: "text", text: "First prompt" },
{ type: "text", text: "Recovery prompt" },
],
},
...(request.messages as unknown[]),
];

await withProxy("anthropic-messages", async (proxyUrl) => {
const response = await postJson(
proxyUrl,
endpoints["anthropic-messages"],
request,
);
expect(response.status).toBe(200);
});
});

test.each(backends)(
"replays compaction responses through %s",
async (backend) => {
Expand Down
8 changes: 5 additions & 3 deletions test/harness/replayingCapiProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
chatCompletionResponseToAnthropicMessage,
chatCompletionResponseToAnthropicSseChunks,
} from "./anthropicMessagesAdapter";
import { canonicalUserMessageSeparator } from "./modelProtocolAdapterShared";
import {
chatCompletionResponseToResponsesApiMessage,
chatCompletionResponseToResponsesApiSseChunks,
Expand Down Expand Up @@ -983,7 +984,7 @@ function coalesceAdjacentUserMessages(requestBody: string): string {
typeof previous.content === "string" &&
typeof message.content === "string"
) {
previous.content = `${previous.content.trimEnd()}\n\n\n${message.content.trimStart()}`;
previous.content = `${previous.content.trimEnd()}${canonicalUserMessageSeparator}${message.content.trimStart()}`;
} else {
messages.push(message);
}
Expand All @@ -993,7 +994,7 @@ function coalesceAdjacentUserMessages(requestBody: string): string {
if (message.role === "user" && typeof message.content === "string") {
message.content = normalizeUserMessage(message.content).replace(
/\n{5,}/g,
"\n\n\n",
canonicalUserMessageSeparator,
);
}
}
Expand Down Expand Up @@ -1348,7 +1349,8 @@ function coalesceMessages(
continue;
}

const separator = message.role === "user" ? "\n\n\n" : "";
const separator =
message.role === "user" ? canonicalUserMessageSeparator : "";
const previousContent = previous.content ?? "";
const currentContent = message.content ?? "";
const content = `${previousContent}${previousContent && currentContent ? separator : ""}${currentContent}`;
Expand Down
Loading