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
52 changes: 40 additions & 12 deletions src/routes/messages/non-stream-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type AnthropicMessagesPayload,
type AnthropicResponse,
type AnthropicTextBlock,
type AnthropicThinkingBlock,
type AnthropicTool,
type AnthropicToolResultBlock,
type AnthropicToolUseBlock,
Expand Down Expand Up @@ -131,11 +132,21 @@ function handleAssistantMessage(
(block): block is AnthropicTextBlock => block.type === "text",
)

const thinkingBlocks = message.content.filter(
(block): block is AnthropicThinkingBlock => block.type === "thinking",
)

// Combine text and thinking blocks, as OpenAI doesn't have separate thinking blocks
const allTextContent = [
...textBlocks.map((b) => b.text),
...thinkingBlocks.map((b) => b.thinking),
].join("\n\n")

return toolUseBlocks.length > 0 ?
[
{
role: "assistant",
content: textBlocks.map((b) => b.text).join("\n\n") || null,
content: allTextContent || null,
tool_calls: toolUseBlocks.map((toolUse) => ({
id: toolUse.id,
type: "function",
Expand Down Expand Up @@ -169,22 +180,38 @@ function mapContent(
const hasImage = content.some((block) => block.type === "image")
if (!hasImage) {
return content
.filter((block): block is AnthropicTextBlock => block.type === "text")
.map((block) => block.text)
.filter(
(block): block is AnthropicTextBlock | AnthropicThinkingBlock =>
block.type === "text" || block.type === "thinking",
)
.map((block) => (block.type === "text" ? block.text : block.thinking))
.join("\n\n")
}

const contentParts: Array<ContentPart> = []
for (const block of content) {
if (block.type === "text") {
contentParts.push({ type: "text", text: block.text })
} else if (block.type === "image") {
contentParts.push({
type: "image_url",
image_url: {
url: `data:${block.source.media_type};base64,${block.source.data}`,
},
})
switch (block.type) {
case "text": {
contentParts.push({ type: "text", text: block.text })

break
}
case "thinking": {
contentParts.push({ type: "text", text: block.thinking })

break
}
case "image": {
contentParts.push({
type: "image_url",
image_url: {
url: `data:${block.source.media_type};base64,${block.source.data}`,
},
})

break
}
// No default
}
}
return contentParts
Expand Down Expand Up @@ -246,6 +273,7 @@ export function translateToAnthropic(
const choice = response.choices[0]
const textBlocks = getAnthropicTextBlocks(choice.message.content)
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls)
// Note: GitHub Copilot doesn't generate thinking blocks, so we don't include them in responses

return {
id: response.id,
Expand Down
73 changes: 73 additions & 0 deletions tests/anthropic-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,79 @@ describe("Anthropic to OpenAI translation logic", () => {
// Should fail validation
expect(isValidChatCompletionRequest(openAIPayload)).toBe(false)
})

test("should handle thinking blocks in assistant messages", () => {
const anthropicPayload: AnthropicMessagesPayload = {
model: "claude-3-5-sonnet-20241022",
messages: [
{ role: "user", content: "What is 2+2?" },
{
role: "assistant",
content: [
{
type: "thinking",
thinking: "Let me think about this simple math problem...",
},
{ type: "text", text: "2+2 equals 4." },
],
},
],
max_tokens: 100,
}
const openAIPayload = translateToOpenAI(anthropicPayload)
expect(isValidChatCompletionRequest(openAIPayload)).toBe(true)

// Check that thinking content is combined with text content
const assistantMessage = openAIPayload.messages.find(
(m) => m.role === "assistant",
)
expect(assistantMessage?.content).toContain(
"Let me think about this simple math problem...",
)
expect(assistantMessage?.content).toContain("2+2 equals 4.")
})

test("should handle thinking blocks with tool calls", () => {
const anthropicPayload: AnthropicMessagesPayload = {
model: "claude-3-5-sonnet-20241022",
messages: [
{ role: "user", content: "What's the weather?" },
{
role: "assistant",
content: [
{
type: "thinking",
thinking:
"I need to call the weather API to get current weather information.",
},
{ type: "text", text: "I'll check the weather for you." },
{
type: "tool_use",
id: "call_123",
name: "get_weather",
input: { location: "New York" },
},
],
},
],
max_tokens: 100,
}
const openAIPayload = translateToOpenAI(anthropicPayload)
expect(isValidChatCompletionRequest(openAIPayload)).toBe(true)

// Check that thinking content is included in the message content
const assistantMessage = openAIPayload.messages.find(
(m) => m.role === "assistant",
)
expect(assistantMessage?.content).toContain(
"I need to call the weather API",
)
expect(assistantMessage?.content).toContain(
"I'll check the weather for you.",
)
expect(assistantMessage?.tool_calls).toHaveLength(1)
expect(assistantMessage?.tool_calls?.[0].function.name).toBe("get_weather")
})
})

describe("OpenAI Chat Completion v1 Request Payload Validation with Zod", () => {
Expand Down