Skip to content

Commit 6a40e43

Browse files
authored
Merge pull request #72 from lanej/feat/thinking-blocks-support
fix: handle Anthropic thinking blocks to prevent 400 errors
2 parents d2a9e2d + 52b8cb1 commit 6a40e43

File tree

2 files changed

+113
-12
lines changed

2 files changed

+113
-12
lines changed

src/routes/messages/non-stream-translation.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type AnthropicMessagesPayload,
1616
type AnthropicResponse,
1717
type AnthropicTextBlock,
18+
type AnthropicThinkingBlock,
1819
type AnthropicTool,
1920
type AnthropicToolResultBlock,
2021
type AnthropicToolUseBlock,
@@ -131,11 +132,21 @@ function handleAssistantMessage(
131132
(block): block is AnthropicTextBlock => block.type === "text",
132133
)
133134

135+
const thinkingBlocks = message.content.filter(
136+
(block): block is AnthropicThinkingBlock => block.type === "thinking",
137+
)
138+
139+
// Combine text and thinking blocks, as OpenAI doesn't have separate thinking blocks
140+
const allTextContent = [
141+
...textBlocks.map((b) => b.text),
142+
...thinkingBlocks.map((b) => b.thinking),
143+
].join("\n\n")
144+
134145
return toolUseBlocks.length > 0 ?
135146
[
136147
{
137148
role: "assistant",
138-
content: textBlocks.map((b) => b.text).join("\n\n") || null,
149+
content: allTextContent || null,
139150
tool_calls: toolUseBlocks.map((toolUse) => ({
140151
id: toolUse.id,
141152
type: "function",
@@ -169,22 +180,38 @@ function mapContent(
169180
const hasImage = content.some((block) => block.type === "image")
170181
if (!hasImage) {
171182
return content
172-
.filter((block): block is AnthropicTextBlock => block.type === "text")
173-
.map((block) => block.text)
183+
.filter(
184+
(block): block is AnthropicTextBlock | AnthropicThinkingBlock =>
185+
block.type === "text" || block.type === "thinking",
186+
)
187+
.map((block) => (block.type === "text" ? block.text : block.thinking))
174188
.join("\n\n")
175189
}
176190

177191
const contentParts: Array<ContentPart> = []
178192
for (const block of content) {
179-
if (block.type === "text") {
180-
contentParts.push({ type: "text", text: block.text })
181-
} else if (block.type === "image") {
182-
contentParts.push({
183-
type: "image_url",
184-
image_url: {
185-
url: `data:${block.source.media_type};base64,${block.source.data}`,
186-
},
187-
})
193+
switch (block.type) {
194+
case "text": {
195+
contentParts.push({ type: "text", text: block.text })
196+
197+
break
198+
}
199+
case "thinking": {
200+
contentParts.push({ type: "text", text: block.thinking })
201+
202+
break
203+
}
204+
case "image": {
205+
contentParts.push({
206+
type: "image_url",
207+
image_url: {
208+
url: `data:${block.source.media_type};base64,${block.source.data}`,
209+
},
210+
})
211+
212+
break
213+
}
214+
// No default
188215
}
189216
}
190217
return contentParts
@@ -246,6 +273,7 @@ export function translateToAnthropic(
246273
const choice = response.choices[0]
247274
const textBlocks = getAnthropicTextBlocks(choice.message.content)
248275
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls)
276+
// Note: GitHub Copilot doesn't generate thinking blocks, so we don't include them in responses
249277

250278
return {
251279
id: response.id,

tests/anthropic-request.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,79 @@ describe("Anthropic to OpenAI translation logic", () => {
124124
// Should fail validation
125125
expect(isValidChatCompletionRequest(openAIPayload)).toBe(false)
126126
})
127+
128+
test("should handle thinking blocks in assistant messages", () => {
129+
const anthropicPayload: AnthropicMessagesPayload = {
130+
model: "claude-3-5-sonnet-20241022",
131+
messages: [
132+
{ role: "user", content: "What is 2+2?" },
133+
{
134+
role: "assistant",
135+
content: [
136+
{
137+
type: "thinking",
138+
thinking: "Let me think about this simple math problem...",
139+
},
140+
{ type: "text", text: "2+2 equals 4." },
141+
],
142+
},
143+
],
144+
max_tokens: 100,
145+
}
146+
const openAIPayload = translateToOpenAI(anthropicPayload)
147+
expect(isValidChatCompletionRequest(openAIPayload)).toBe(true)
148+
149+
// Check that thinking content is combined with text content
150+
const assistantMessage = openAIPayload.messages.find(
151+
(m) => m.role === "assistant",
152+
)
153+
expect(assistantMessage?.content).toContain(
154+
"Let me think about this simple math problem...",
155+
)
156+
expect(assistantMessage?.content).toContain("2+2 equals 4.")
157+
})
158+
159+
test("should handle thinking blocks with tool calls", () => {
160+
const anthropicPayload: AnthropicMessagesPayload = {
161+
model: "claude-3-5-sonnet-20241022",
162+
messages: [
163+
{ role: "user", content: "What's the weather?" },
164+
{
165+
role: "assistant",
166+
content: [
167+
{
168+
type: "thinking",
169+
thinking:
170+
"I need to call the weather API to get current weather information.",
171+
},
172+
{ type: "text", text: "I'll check the weather for you." },
173+
{
174+
type: "tool_use",
175+
id: "call_123",
176+
name: "get_weather",
177+
input: { location: "New York" },
178+
},
179+
],
180+
},
181+
],
182+
max_tokens: 100,
183+
}
184+
const openAIPayload = translateToOpenAI(anthropicPayload)
185+
expect(isValidChatCompletionRequest(openAIPayload)).toBe(true)
186+
187+
// Check that thinking content is included in the message content
188+
const assistantMessage = openAIPayload.messages.find(
189+
(m) => m.role === "assistant",
190+
)
191+
expect(assistantMessage?.content).toContain(
192+
"I need to call the weather API",
193+
)
194+
expect(assistantMessage?.content).toContain(
195+
"I'll check the weather for you.",
196+
)
197+
expect(assistantMessage?.tool_calls).toHaveLength(1)
198+
expect(assistantMessage?.tool_calls?.[0].function.name).toBe("get_weather")
199+
})
127200
})
128201

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

0 commit comments

Comments
 (0)