Skip to content

Commit b85665e

Browse files
authored
Merge pull request #18 from caozhiyuan/feature/chat-completions-reasoning
Feature/chat completions reasoning
2 parents 51f28ff + 0d6f7aa commit b85665e

6 files changed

Lines changed: 83 additions & 30 deletions

File tree

src/lib/api-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ export const standardHeaders = () => ({
77
accept: "application/json",
88
})
99

10-
const COPILOT_VERSION = "0.26.7"
10+
const COPILOT_VERSION = "0.35.0"
1111
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`
1212
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
1313

14-
const API_VERSION = "2025-04-01"
14+
const API_VERSION = "2025-10-01"
1515

1616
export const copilotBaseUrl = (state: State) =>
1717
state.accountType === "individual" ?

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

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { Model } from "~/services/copilot/get-models"
2+
3+
import { state } from "~/lib/state"
14
import {
25
type ChatCompletionResponse,
36
type ChatCompletionsPayload,
@@ -29,11 +32,15 @@ import { mapOpenAIStopReasonToAnthropic } from "./utils"
2932
export function translateToOpenAI(
3033
payload: AnthropicMessagesPayload,
3134
): ChatCompletionsPayload {
35+
const modelId = translateModelName(payload.model)
36+
const model = state.models?.data.find((m) => m.id === modelId)
37+
const thinkingBudget = getThinkingBudget(payload, model)
3238
return {
33-
model: translateModelName(payload.model),
39+
model: modelId,
3440
messages: translateAnthropicMessagesToOpenAI(
3541
payload.messages,
3642
payload.system,
43+
modelId,
3744
),
3845
max_tokens: payload.max_tokens,
3946
stop: payload.stop_sequences,
@@ -43,14 +50,36 @@ export function translateToOpenAI(
4350
user: payload.metadata?.user_id,
4451
tools: translateAnthropicToolsToOpenAI(payload.tools),
4552
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice),
53+
thinking_budget: thinkingBudget,
4654
}
4755
}
4856

57+
function getThinkingBudget(
58+
payload: AnthropicMessagesPayload,
59+
model: Model | undefined,
60+
): number | undefined {
61+
const thinking = payload.thinking
62+
if (model && thinking) {
63+
const maxThinkingBudget = Math.min(
64+
model.capabilities.supports.max_thinking_budget ?? 0,
65+
(model.capabilities.limits.max_output_tokens ?? 0) - 1,
66+
)
67+
if (maxThinkingBudget > 0 && thinking.budget_tokens !== undefined) {
68+
const budgetTokens = Math.min(thinking.budget_tokens, maxThinkingBudget)
69+
return Math.max(
70+
budgetTokens,
71+
model.capabilities.supports.min_thinking_budget ?? 1024,
72+
)
73+
}
74+
}
75+
return undefined
76+
}
77+
4978
function translateModelName(model: string): string {
5079
// Subagent requests use a specific model number which Copilot doesn't support
5180
if (model.startsWith("claude-sonnet-4-")) {
5281
return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4")
53-
} else if (model.startsWith("claude-opus-")) {
82+
} else if (model.startsWith("claude-opus-4-")) {
5483
return model.replace(/^claude-opus-4-.*/, "claude-opus-4")
5584
}
5685
return model
@@ -59,13 +88,14 @@ function translateModelName(model: string): string {
5988
function translateAnthropicMessagesToOpenAI(
6089
anthropicMessages: Array<AnthropicMessage>,
6190
system: string | Array<AnthropicTextBlock> | undefined,
91+
modelId: string,
6292
): Array<Message> {
6393
const systemMessages = handleSystemPrompt(system)
6494

6595
const otherMessages = anthropicMessages.flatMap((message) =>
6696
message.role === "user" ?
6797
handleUserMessage(message)
68-
: handleAssistantMessage(message),
98+
: handleAssistantMessage(message, modelId),
6999
)
70100

71101
return [...systemMessages, ...otherMessages]
@@ -125,6 +155,7 @@ function handleUserMessage(message: AnthropicUserMessage): Array<Message> {
125155

126156
function handleAssistantMessage(
127157
message: AnthropicAssistantMessage,
158+
modelId: string,
128159
): Array<Message> {
129160
if (!Array.isArray(message.content)) {
130161
return [
@@ -139,14 +170,28 @@ function handleAssistantMessage(
139170
(block): block is AnthropicToolUseBlock => block.type === "tool_use",
140171
)
141172

142-
const thinkingBlocks = message.content.filter(
173+
let thinkingBlocks = message.content.filter(
143174
(block): block is AnthropicThinkingBlock => block.type === "thinking",
144175
)
145176

146-
const allThinkingContent = thinkingBlocks
177+
if (modelId.startsWith("claude")) {
178+
thinkingBlocks = thinkingBlocks.filter(
179+
(b) =>
180+
b.thinking
181+
&& b.thinking.length > 0
182+
&& b.signature
183+
&& b.signature.length > 0
184+
// gpt signature has @ in it, so filter those out for claude models
185+
&& !b.signature.includes("@"),
186+
)
187+
}
188+
189+
const thinkingContents = thinkingBlocks
147190
.filter((b) => b.thinking && b.thinking.length > 0)
148191
.map((b) => b.thinking)
149-
.join("\n\n")
192+
193+
const allThinkingContent =
194+
thinkingContents.length > 0 ? thinkingContents.join("\n\n") : undefined
150195

151196
const signature = thinkingBlocks.find(
152197
(b) => b.signature && b.signature.length > 0,
@@ -281,13 +326,13 @@ export function translateToAnthropic(
281326
// Process all choices to extract text and tool use blocks
282327
for (const choice of response.choices) {
283328
const textBlocks = getAnthropicTextBlocks(choice.message.content)
284-
const thingBlocks = getAnthropicThinkBlocks(
329+
const thinkBlocks = getAnthropicThinkBlocks(
285330
choice.message.reasoning_text,
286331
choice.message.reasoning_opaque,
287332
)
288333
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls)
289334

290-
assistantContentBlocks.push(...thingBlocks, ...textBlocks, ...toolUseBlocks)
335+
assistantContentBlocks.push(...thinkBlocks, ...textBlocks, ...toolUseBlocks)
291336

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

src/routes/messages/stream-translation.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,30 @@ function handleContent(
212212
},
213213
})
214214
}
215+
216+
// handle for claude model
217+
if (
218+
delta.content === ""
219+
&& delta.reasoning_opaque
220+
&& delta.reasoning_opaque.length > 0
221+
) {
222+
events.push(
223+
{
224+
type: "content_block_delta",
225+
index: state.contentBlockIndex,
226+
delta: {
227+
type: "signature_delta",
228+
signature: delta.reasoning_opaque,
229+
},
230+
},
231+
{
232+
type: "content_block_stop",
233+
index: state.contentBlockIndex,
234+
},
235+
)
236+
state.contentBlockIndex++
237+
state.thinkingBlockOpen = false
238+
}
215239
}
216240

217241
function handleMessageStart(
@@ -313,25 +337,6 @@ function handleThinkingText(
313337
thinking: delta.reasoning_text,
314338
},
315339
})
316-
317-
if (delta.reasoning_opaque && delta.reasoning_opaque.length > 0) {
318-
events.push(
319-
{
320-
type: "content_block_delta",
321-
index: state.contentBlockIndex,
322-
delta: {
323-
type: "signature_delta",
324-
signature: delta.reasoning_opaque,
325-
},
326-
},
327-
{
328-
type: "content_block_stop",
329-
index: state.contentBlockIndex,
330-
},
331-
)
332-
state.contentBlockIndex++
333-
state.thinkingBlockOpen = false
334-
}
335340
}
336341
}
337342

src/services/copilot/create-chat-completions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export interface ChatCompletionsPayload {
152152
| { type: "function"; function: { name: string } }
153153
| null
154154
user?: string | null
155+
thinking_budget?: number
155156
}
156157

157158
export interface Tool {

src/services/copilot/get-models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ interface ModelLimits {
2525
}
2626

2727
interface ModelSupports {
28+
max_thinking_budget?: number
29+
min_thinking_budget?: number
2830
tool_calls?: boolean
2931
parallel_tool_calls?: boolean
3032
dimensions?: boolean

src/services/get-vscode-version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const FALLBACK = "1.104.3"
1+
const FALLBACK = "1.107.0"
22

33
export async function getVSCodeVersion() {
44
const controller = new AbortController()

0 commit comments

Comments
 (0)