Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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: 3 additions & 0 deletions src/lib/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const calculateMessageTokens = (
const tokensPerName = 1
let tokens = tokensPerMessage
for (const [key, value] of Object.entries(message)) {
if (key === "reasoning_opaque") {
continue
}
if (typeof value === "string") {
tokens += encoder.encode(value).length
}
Expand Down
2 changes: 2 additions & 0 deletions src/routes/messages/anthropic-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface AnthropicToolUseBlock {
export interface AnthropicThinkingBlock {
type: "thinking"
thinking: string
signature: string
}

export type AnthropicUserContentBlock =
Expand Down Expand Up @@ -196,6 +197,7 @@ export interface AnthropicStreamState {
messageStartSent: boolean
contentBlockIndex: number
contentBlockOpen: boolean
thinkingBlockOpen: boolean
toolCalls: {
[openAIToolIndex: number]: {
id: string
Expand Down
1 change: 1 addition & 0 deletions src/routes/messages/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function handleCompletion(c: Context) {
contentBlockIndex: 0,
contentBlockOpen: false,
toolCalls: {},
thinkingBlockOpen: false,
}

for await (const rawEvent of response) {
Expand Down
82 changes: 49 additions & 33 deletions src/routes/messages/non-stream-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,25 +139,26 @@ function handleAssistantMessage(
(block): block is AnthropicToolUseBlock => block.type === "tool_use",
)

const textBlocks = message.content.filter(
(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")
const allThinkingContent = thinkingBlocks
.filter((b) => b.thinking && b.thinking.length > 0)
.map((b) => b.thinking)
.join("\n\n")

const signature = thinkingBlocks.find(
(b) => b.signature && b.signature.length > 0,
)?.signature

return toolUseBlocks.length > 0 ?
[
{
role: "assistant",
content: allTextContent || null,
content: mapContent(message.content),
reasoning_text: allThinkingContent,
reasoning_opaque: signature,
tool_calls: toolUseBlocks.map((toolUse) => ({
id: toolUse.id,
type: "function",
Expand All @@ -172,6 +173,8 @@ function handleAssistantMessage(
{
role: "assistant",
content: mapContent(message.content),
reasoning_text: allThinkingContent,
reasoning_opaque: signature,
},
]
}
Expand All @@ -191,11 +194,8 @@ function mapContent(
const hasImage = content.some((block) => block.type === "image")
if (!hasImage) {
return content
.filter(
(block): block is AnthropicTextBlock | AnthropicThinkingBlock =>
block.type === "text" || block.type === "thinking",
)
.map((block) => (block.type === "text" ? block.text : block.thinking))
.filter((block): block is AnthropicTextBlock => block.type === "text")
.map((block) => block.text)
.join("\n\n")
}

Expand All @@ -204,12 +204,6 @@ function mapContent(
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": {
Expand All @@ -219,7 +213,6 @@ function mapContent(
url: `data:${block.source.media_type};base64,${block.source.data}`,
},
})

break
}
// No default
Expand Down Expand Up @@ -282,34 +275,32 @@ export function translateToAnthropic(
response: ChatCompletionResponse,
): AnthropicResponse {
// Merge content from all choices
const allTextBlocks: Array<AnthropicTextBlock> = []
const allToolUseBlocks: Array<AnthropicToolUseBlock> = []
let stopReason: "stop" | "length" | "tool_calls" | "content_filter" | null =
null // default
stopReason = response.choices[0]?.finish_reason ?? stopReason
const assistantContentBlocks: Array<AnthropicAssistantContentBlock> = []
let stopReason = response.choices[0]?.finish_reason ?? null

// Process all choices to extract text and tool use blocks
for (const choice of response.choices) {
const textBlocks = getAnthropicTextBlocks(choice.message.content)
const thingBlocks = getAnthropicThinkBlocks(
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name thingBlocks appears to be a typo and should be thinkBlocks or thinkingBlocks to be consistent with the function name getAnthropicThinkBlocks and the purpose (processing thinking blocks).

Copilot uses AI. Check for mistakes.
choice.message.reasoning_text,
choice.message.reasoning_opaque,
)
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls)

allTextBlocks.push(...textBlocks)
allToolUseBlocks.push(...toolUseBlocks)
assistantContentBlocks.push(...thingBlocks, ...textBlocks, ...toolUseBlocks)

// Use the finish_reason from the first choice, or prioritize tool_calls
if (choice.finish_reason === "tool_calls" || stopReason === "stop") {
stopReason = choice.finish_reason
}
}

// Note: GitHub Copilot doesn't generate thinking blocks, so we don't include them in responses

return {
id: response.id,
type: "message",
role: "assistant",
model: response.model,
content: [...allTextBlocks, ...allToolUseBlocks],
content: assistantContentBlocks,
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
stop_sequence: null,
usage: {
Expand All @@ -329,7 +320,7 @@ export function translateToAnthropic(
function getAnthropicTextBlocks(
messageContent: Message["content"],
): Array<AnthropicTextBlock> {
if (typeof messageContent === "string") {
if (typeof messageContent === "string" && messageContent.length > 0) {
return [{ type: "text", text: messageContent }]
}

Expand All @@ -342,6 +333,31 @@ function getAnthropicTextBlocks(
return []
}

function getAnthropicThinkBlocks(
reasoningText: string | null | undefined,
reasoningOpaque: string | null | undefined,
): Array<AnthropicThinkingBlock> {
if (reasoningText && reasoningText.length > 0) {
return [
{
type: "thinking",
thinking: reasoningText,
signature: reasoningOpaque || "",
},
]
}
if (reasoningOpaque && reasoningOpaque.length > 0) {
return [
{
type: "thinking",
thinking: "",
signature: reasoningOpaque,
},
]
}
return []
}

function getAnthropicToolUseBlocks(
toolCalls: Array<ToolCall> | undefined,
): Array<AnthropicToolUseBlock> {
Expand Down
Loading