Skip to content
Merged

czy #20

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
7 changes: 4 additions & 3 deletions src/lib/api-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
const API_VERSION = "2025-10-01"

export const copilotBaseUrl = (state: State) =>
`https://api.${state.accountType}.githubcopilot.com`

state.accountType === "individual" ?
"https://api.githubcopilot.com"
: `https://api.${state.accountType}.githubcopilot.com`
export const copilotHeaders = (state: State, vision: boolean = false) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${state.copilotToken}`,
Expand All @@ -24,7 +25,7 @@ export const copilotHeaders = (state: State, vision: boolean = false) => {
"editor-version": `vscode/${state.vsCodeVersion}`,
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
"user-agent": USER_AGENT,
"openai-intent": "conversation-panel",
"openai-intent": "conversation-agent",
"x-github-api-version": API_VERSION,
"x-request-id": randomUUID(),
"x-vscode-user-agent-library-version": "electron-fetch",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface AppConfig {
string,
"none" | "minimal" | "low" | "medium" | "high" | "xhigh"
>
useFunctionApplyPatch?: boolean
}

const gpt5ExplorationPrompt = `## Exploration and reading files
Expand All @@ -28,6 +29,7 @@ const defaultConfig: AppConfig = {
modelReasoningEfforts: {
"gpt-5-mini": "low",
},
useFunctionApplyPatch: true,
}

let cachedConfig: AppConfig | null = null
Expand Down
63 changes: 52 additions & 11 deletions src/routes/messages/non-stream-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import {
type AnthropicAssistantContentBlock,
type AnthropicAssistantMessage,
type AnthropicMessage,
type AnthropicMessagesPayload,
type AnthropicResponse,
type AnthropicTextBlock,
Expand All @@ -38,9 +37,9 @@ export function translateToOpenAI(
return {
model: modelId,
messages: translateAnthropicMessagesToOpenAI(
payload.messages,
payload.system,
payload,
modelId,
thinkingBudget,
),
max_tokens: payload.max_tokens,
stop: payload.stop_sequences,
Expand Down Expand Up @@ -86,32 +85,74 @@ function translateModelName(model: string): string {
}

function translateAnthropicMessagesToOpenAI(
anthropicMessages: Array<AnthropicMessage>,
system: string | Array<AnthropicTextBlock> | undefined,
payload: AnthropicMessagesPayload,
modelId: string,
thinkingBudget: number | undefined,
): Array<Message> {
const systemMessages = handleSystemPrompt(system)

const otherMessages = anthropicMessages.flatMap((message) =>
const systemMessages = handleSystemPrompt(
payload.system,
modelId,
thinkingBudget,
)
const otherMessages = payload.messages.flatMap((message) =>
message.role === "user" ?
handleUserMessage(message)
: handleAssistantMessage(message, modelId),
)

if (modelId.startsWith("claude") && thinkingBudget) {
const reminder =
"<system-reminder>you MUST follow interleaved_thinking_protocol</system-reminder>"
const firstUserIndex = otherMessages.findIndex((m) => m.role === "user")
if (firstUserIndex !== -1) {
const userMessage = otherMessages[firstUserIndex]
if (typeof userMessage.content === "string") {
userMessage.content = reminder + "\n\n" + userMessage.content
} else if (Array.isArray(userMessage.content)) {
userMessage.content = [
{ type: "text", text: reminder },
...userMessage.content,
] as Array<ContentPart>
}
}
}
return [...systemMessages, ...otherMessages]
}

function handleSystemPrompt(
system: string | Array<AnthropicTextBlock> | undefined,
modelId: string,
thinkingBudget: number | undefined,
): Array<Message> {
if (!system) {
return []
}

let extraPrompt = ""
if (modelId.startsWith("claude") && thinkingBudget) {
extraPrompt = `
<interleaved_thinking_protocol>
ABSOLUTE REQUIREMENT - NON-NEGOTIABLE:
The current thinking_mode is interleaved, Whenever you have the result of a function call, think carefully , MUST output a thinking block
RULES:
Tool result → thinking block (ALWAYS, no exceptions)
This is NOT optional - it is a hard requirement
The thinking block must contain substantive reasoning (minimum 3-5 sentences)
Think about: what the results mean, what to do next, how to answer the user
NEVER skip this step, even if the result seems simple or obvious
</interleaved_thinking_protocol>`
}

if (typeof system === "string") {
return [{ role: "system", content: system }]
return [{ role: "system", content: system + extraPrompt }]
} else {
const systemText = system.map((block) => block.text).join("\n\n")
const systemText = system
.map((block, index) => {
if (index === 0) {
return block.text + extraPrompt
}
return block.text
})
.join("\n\n")
return [{ role: "system", content: systemText }]
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/routes/messages/stream-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ function handleContent(
delta.content === ""
&& delta.reasoning_opaque
&& delta.reasoning_opaque.length > 0
&& state.thinkingBlockOpen
) {
events.push(
{
Expand Down Expand Up @@ -317,6 +318,15 @@ function handleThinkingText(
events: Array<AnthropicStreamEventData>,
) {
if (delta.reasoning_text && delta.reasoning_text.length > 0) {
// compatible with copilot API returning content->reasoning_text->reasoning_opaque in different deltas
// this is an extremely abnormal situation, probably a server-side bug
// only occurs in the claude model, with a very low probability of occurrence
if (state.contentBlockOpen) {
delta.content = delta.reasoning_text
delta.reasoning_text = undefined
return
}

if (!state.thinkingBlockOpen) {
events.push({
type: "content_block_start",
Expand Down
35 changes: 35 additions & 0 deletions src/routes/responses/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Context } from "hono"
import { streamSSE } from "hono/streaming"

import { awaitApproval } from "~/lib/approval"
import { getConfig } from "~/lib/config"
import { createHandlerLogger } from "~/lib/logger"
import { checkRateLimit } from "~/lib/rate-limit"
import { state } from "~/lib/state"
Expand All @@ -24,6 +25,8 @@ export const handleResponses = async (c: Context) => {
const payload = await c.req.json<ResponsesPayload>()
logger.debug("Responses request payload:", JSON.stringify(payload))

useFunctionApplyPatch(payload)

const selectedModel = state.models?.data.find(
(model) => model.id === payload.model,
)
Expand Down Expand Up @@ -78,3 +81,35 @@ const isAsyncIterable = <T>(value: unknown): value is AsyncIterable<T> =>

const isStreamingRequested = (payload: ResponsesPayload): boolean =>
Boolean(payload.stream)

const useFunctionApplyPatch = (payload: ResponsesPayload): void => {
const config = getConfig()
const useFunctionApplyPatch = config.useFunctionApplyPatch ?? true
if (useFunctionApplyPatch) {
logger.debug("Using function tool apply_patch for responses")
if (Array.isArray(payload.tools)) {
const toolsArr = payload.tools
for (let i = 0; i < toolsArr.length; i++) {
const t = toolsArr[i]
if (t.type === "custom" && t.name === "apply_patch") {
toolsArr[i] = {
type: "function",
name: t.name,
description: "Use the `apply_patch` tool to edit files",
parameters: {
type: "object",
properties: {
input: {
type: "string",
description: "The entire contents of the apply_patch command",
},
},
required: ["input"],
},
strict: false,
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/services/copilot/create-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface ToolChoiceFunction {
type: "function"
}

export type Tool = FunctionTool
export type Tool = FunctionTool | Record<string, unknown>

export interface FunctionTool {
name: string
Expand Down
Loading