Skip to content

Commit 51b1e23

Browse files
authored
Merge pull request ericc-ch#41 from caozhiyuan/feature/chat-completions-reasoning
Feature/chat completions reasoning
2 parents 3cdc32c + dfb40d2 commit 51b1e23

File tree

8 files changed

+305
-96
lines changed

8 files changed

+305
-96
lines changed

src/lib/tokenizer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ const calculateMessageTokens = (
7575
const tokensPerName = 1
7676
let tokens = tokensPerMessage
7777
for (const [key, value] of Object.entries(message)) {
78+
if (key === "reasoning_opaque") {
79+
continue
80+
}
7881
if (typeof value === "string") {
7982
tokens += encoder.encode(value).length
8083
}

src/routes/messages/anthropic-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export interface AnthropicStreamState {
197197
messageStartSent: boolean
198198
contentBlockIndex: number
199199
contentBlockOpen: boolean
200+
thinkingBlockOpen: boolean
200201
toolCalls: {
201202
[openAIToolIndex: number]: {
202203
id: string

src/routes/messages/handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const handleWithChatCompletions = async (
9898
contentBlockIndex: 0,
9999
contentBlockOpen: false,
100100
toolCalls: {},
101+
thinkingBlockOpen: false,
101102
}
102103

103104
for await (const rawEvent of response) {

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

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -139,25 +139,26 @@ function handleAssistantMessage(
139139
(block): block is AnthropicToolUseBlock => block.type === "tool_use",
140140
)
141141

142-
const textBlocks = message.content.filter(
143-
(block): block is AnthropicTextBlock => block.type === "text",
144-
)
145-
146142
const thinkingBlocks = message.content.filter(
147143
(block): block is AnthropicThinkingBlock => block.type === "thinking",
148144
)
149145

150-
// Combine text and thinking blocks, as OpenAI doesn't have separate thinking blocks
151-
const allTextContent = [
152-
...textBlocks.map((b) => b.text),
153-
...thinkingBlocks.map((b) => b.thinking),
154-
].join("\n\n")
146+
const allThinkingContent = thinkingBlocks
147+
.filter((b) => b.thinking && b.thinking.length > 0)
148+
.map((b) => b.thinking)
149+
.join("\n\n")
150+
151+
const signature = thinkingBlocks.find(
152+
(b) => b.signature && b.signature.length > 0,
153+
)?.signature
155154

156155
return toolUseBlocks.length > 0 ?
157156
[
158157
{
159158
role: "assistant",
160-
content: allTextContent || null,
159+
content: mapContent(message.content),
160+
reasoning_text: allThinkingContent,
161+
reasoning_opaque: signature,
161162
tool_calls: toolUseBlocks.map((toolUse) => ({
162163
id: toolUse.id,
163164
type: "function",
@@ -172,6 +173,8 @@ function handleAssistantMessage(
172173
{
173174
role: "assistant",
174175
content: mapContent(message.content),
176+
reasoning_text: allThinkingContent,
177+
reasoning_opaque: signature,
175178
},
176179
]
177180
}
@@ -191,11 +194,8 @@ function mapContent(
191194
const hasImage = content.some((block) => block.type === "image")
192195
if (!hasImage) {
193196
return content
194-
.filter(
195-
(block): block is AnthropicTextBlock | AnthropicThinkingBlock =>
196-
block.type === "text" || block.type === "thinking",
197-
)
198-
.map((block) => (block.type === "text" ? block.text : block.thinking))
197+
.filter((block): block is AnthropicTextBlock => block.type === "text")
198+
.map((block) => block.text)
199199
.join("\n\n")
200200
}
201201

@@ -204,12 +204,6 @@ function mapContent(
204204
switch (block.type) {
205205
case "text": {
206206
contentParts.push({ type: "text", text: block.text })
207-
208-
break
209-
}
210-
case "thinking": {
211-
contentParts.push({ type: "text", text: block.thinking })
212-
213207
break
214208
}
215209
case "image": {
@@ -219,7 +213,6 @@ function mapContent(
219213
url: `data:${block.source.media_type};base64,${block.source.data}`,
220214
},
221215
})
222-
223216
break
224217
}
225218
// No default
@@ -282,34 +275,32 @@ export function translateToAnthropic(
282275
response: ChatCompletionResponse,
283276
): AnthropicResponse {
284277
// Merge content from all choices
285-
const allTextBlocks: Array<AnthropicTextBlock> = []
286-
const allToolUseBlocks: Array<AnthropicToolUseBlock> = []
287-
let stopReason: "stop" | "length" | "tool_calls" | "content_filter" | null =
288-
null // default
289-
stopReason = response.choices[0]?.finish_reason ?? stopReason
278+
const assistantContentBlocks: Array<AnthropicAssistantContentBlock> = []
279+
let stopReason = response.choices[0]?.finish_reason ?? null
290280

291281
// Process all choices to extract text and tool use blocks
292282
for (const choice of response.choices) {
293283
const textBlocks = getAnthropicTextBlocks(choice.message.content)
284+
const thingBlocks = getAnthropicThinkBlocks(
285+
choice.message.reasoning_text,
286+
choice.message.reasoning_opaque,
287+
)
294288
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls)
295289

296-
allTextBlocks.push(...textBlocks)
297-
allToolUseBlocks.push(...toolUseBlocks)
290+
assistantContentBlocks.push(...thingBlocks, ...textBlocks, ...toolUseBlocks)
298291

299292
// Use the finish_reason from the first choice, or prioritize tool_calls
300293
if (choice.finish_reason === "tool_calls" || stopReason === "stop") {
301294
stopReason = choice.finish_reason
302295
}
303296
}
304297

305-
// Note: GitHub Copilot doesn't generate thinking blocks, so we don't include them in responses
306-
307298
return {
308299
id: response.id,
309300
type: "message",
310301
role: "assistant",
311302
model: response.model,
312-
content: [...allTextBlocks, ...allToolUseBlocks],
303+
content: assistantContentBlocks,
313304
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
314305
stop_sequence: null,
315306
usage: {
@@ -329,7 +320,7 @@ export function translateToAnthropic(
329320
function getAnthropicTextBlocks(
330321
messageContent: Message["content"],
331322
): Array<AnthropicTextBlock> {
332-
if (typeof messageContent === "string") {
323+
if (typeof messageContent === "string" && messageContent.length > 0) {
333324
return [{ type: "text", text: messageContent }]
334325
}
335326

@@ -342,6 +333,31 @@ function getAnthropicTextBlocks(
342333
return []
343334
}
344335

336+
function getAnthropicThinkBlocks(
337+
reasoningText: string | null | undefined,
338+
reasoningOpaque: string | null | undefined,
339+
): Array<AnthropicThinkingBlock> {
340+
if (reasoningText && reasoningText.length > 0) {
341+
return [
342+
{
343+
type: "thinking",
344+
thinking: reasoningText,
345+
signature: reasoningOpaque || "",
346+
},
347+
]
348+
}
349+
if (reasoningOpaque && reasoningOpaque.length > 0) {
350+
return [
351+
{
352+
type: "thinking",
353+
thinking: "",
354+
signature: reasoningOpaque,
355+
},
356+
]
357+
}
358+
return []
359+
}
360+
345361
function getAnthropicToolUseBlocks(
346362
toolCalls: Array<ToolCall> | undefined,
347363
): Array<AnthropicToolUseBlock> {

0 commit comments

Comments
 (0)