Skip to content
Open
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
20 changes: 19 additions & 1 deletion src/lib/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@ import consola from "consola"
import { getProxyForUrl } from "proxy-from-env"
import { Agent, ProxyAgent, setGlobalDispatcher, type Dispatcher } from "undici"

const MAX_CONNECTIONS_PER_ORIGIN = 16

function createDirectAgent(): Agent {
return new Agent({
connections: MAX_CONNECTIONS_PER_ORIGIN,
pipelining: 1,
})
}

export function initDirectConnectionPool(): void {
if (typeof Bun !== "undefined") return

setGlobalDispatcher(createDirectAgent())
consola.debug(
`HTTP connection pool configured with ${MAX_CONNECTIONS_PER_ORIGIN} connections per origin`,
)
}

export function initProxyFromEnv(): void {
if (typeof Bun !== "undefined") return

try {
const direct = new Agent()
const direct = createDirectAgent()
const proxies = new Map<string, ProxyAgent>()

// We only need a minimal dispatcher that implements `dispatch` at runtime.
Expand Down
25 changes: 21 additions & 4 deletions src/lib/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,35 @@ export const setupCopilotToken = async () => {
const refreshInterval = (refresh_in - 60) * 1000
setInterval(async () => {
consola.debug("Refreshing Copilot token")
try {
await refreshCopilotToken()
} catch (error) {
consola.error("Failed to refresh Copilot token:", error)
}
}, refreshInterval)
}

let inflightRefresh: Promise<void> | undefined

/**
* Fetch a new Copilot token and store it in state. Concurrent callers share
* one in-flight request, so a burst of 401/403 responses triggers a single
* refresh.
*/
export const refreshCopilotToken = (): Promise<void> => {
inflightRefresh ??= (async () => {
try {
const { token } = await getCopilotToken()
state.copilotToken = token
consola.debug("Copilot token refreshed")
if (state.showToken) {
consola.info("Refreshed Copilot token:", token)
}
} catch (error) {
consola.error("Failed to refresh Copilot token:", error)
throw error
} finally {
inflightRefresh = undefined
}
}, refreshInterval)
})()
return inflightRefresh
}

interface SetupGitHubTokenOptions {
Expand Down
54 changes: 43 additions & 11 deletions src/routes/chat-completions/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export async function handleCompletion(c: Context) {
await checkRateLimit(state)

let payload = await c.req.json<ChatCompletionsPayload>()
consola.debug("Request payload:", JSON.stringify(payload).slice(-400))
if (consola.level >= 4) {
consola.debug("Request payload:", JSON.stringify(payload).slice(-400))
}

// Find the selected model
const selectedModel = state.models?.data.find(
Expand Down Expand Up @@ -47,20 +49,50 @@ export async function handleCompletion(c: Context) {
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens))
}

if (payload.stream) {
consola.debug("Streaming response")
return streamSSE(
c,
async (stream) => {
const controller = new AbortController()
stream.onAbort(() => {
controller.abort()
})

const response = await createChatCompletions(payload, controller.signal)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve upstream failures before returning an SSE response. Both routes now await the Copilot request inside the streamSSE callback. If the token is missing or Copilot rejects the request, Hono has already returned the SSE response. Hono then writes an error event instead of allowing an HTTP error response. (github.com)

  • src/routes/chat-completions/handler.ts#L62-L62: await the upstream connection before calling streamSSE; retain the controller for disconnect cancellation.
  • src/routes/messages/handler.ts#L58-L61: make the same change before starting the translated SSE response.
📍 Affects 2 files
  • src/routes/chat-completions/handler.ts#L62-L62 (this comment)
  • src/routes/messages/handler.ts#L58-L61

if (isNonStreaming(response)) {
throw new Error("Expected a streaming response from Copilot")
}

try {
for await (const chunk of response) {
if (consola.level >= 4) {
consola.debug("Streaming chunk:", JSON.stringify(chunk))
}
await stream.writeSSE(chunk as SSEMessage)
}
} finally {
controller.abort()
}
},
(error) => {
if (error.name !== "AbortError") {
consola.error("Streaming request failed:", error)
}
return Promise.resolve()
},
)
}

const response = await createChatCompletions(payload)
if (!isNonStreaming(response)) {
throw new Error("Expected a non-streaming response from Copilot")
}

if (isNonStreaming(response)) {
if (consola.level >= 4) {
consola.debug("Non-streaming response:", JSON.stringify(response))
return c.json(response)
}

consola.debug("Streaming response")
return streamSSE(c, async (stream) => {
for await (const chunk of response) {
consola.debug("Streaming chunk:", JSON.stringify(chunk))
await stream.writeSSE(chunk as SSEMessage)
}
})
return c.json(response)
}

const isNonStreaming = (
Expand Down
116 changes: 85 additions & 31 deletions src/routes/messages/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,64 +26,118 @@ export async function handleCompletion(c: Context) {
await checkRateLimit(state)

const anthropicPayload = await c.req.json<AnthropicMessagesPayload>()
consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload))
if (consola.level >= 4) {
consola.debug(
"Anthropic request payload:",
JSON.stringify(anthropicPayload),
)
}

const openAIPayload = translateToOpenAI(anthropicPayload)
consola.debug(
"Translated OpenAI request payload:",
JSON.stringify(openAIPayload),
)
if (consola.level >= 4) {
consola.debug(
"Translated OpenAI request payload:",
JSON.stringify(openAIPayload),
)
}

if (state.manualApprove) {
await awaitApproval()
}

if (openAIPayload.stream) {
consola.debug("Streaming response from Copilot")
return streamSSE(
c,
async (stream) => {
const controller = new AbortController()
stream.onAbort(() => {
controller.abort()
})

const response = await createChatCompletions(
openAIPayload,
controller.signal,
)
if (isNonStreaming(response)) {
throw new Error("Expected a streaming response from Copilot")
}

try {
await writeStreamingResponse(stream, response)
} finally {
controller.abort()
}
},
(error) => {
if (error.name !== "AbortError") {
consola.error("Streaming request failed:", error)
}
return Promise.resolve()
},
)
}

const response = await createChatCompletions(openAIPayload)
if (!isNonStreaming(response)) {
throw new Error("Expected a non-streaming response from Copilot")
}

if (isNonStreaming(response)) {
if (consola.level >= 4) {
consola.debug(
"Non-streaming response from Copilot:",
JSON.stringify(response).slice(-400),
)
const anthropicResponse = translateToAnthropic(response)
}
const anthropicResponse = translateToAnthropic(response)
if (consola.level >= 4) {
consola.debug(
"Translated Anthropic response:",
JSON.stringify(anthropicResponse),
)
return c.json(anthropicResponse)
}
return c.json(anthropicResponse)
}

consola.debug("Streaming response from Copilot")
return streamSSE(c, async (stream) => {
const streamState: AnthropicStreamState = {
messageStartSent: false,
contentBlockIndex: 0,
contentBlockOpen: false,
toolCalls: {},
}
async function writeStreamingResponse(
stream: Parameters<Parameters<typeof streamSSE>[1]>[0],
response: Exclude<
Awaited<ReturnType<typeof createChatCompletions>>,
ChatCompletionResponse
>,
): Promise<void> {
const streamState: AnthropicStreamState = {
messageStartSent: false,
contentBlockIndex: 0,
contentBlockOpen: false,
toolCalls: {},
}

for await (const rawEvent of response) {
for await (const rawEvent of response) {
if (consola.level >= 4) {
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent))
if (rawEvent.data === "[DONE]") {
break
}
}
if (rawEvent.data === "[DONE]") {
break
}

if (!rawEvent.data) {
continue
}
if (!rawEvent.data) {
continue
}

const chunk = JSON.parse(rawEvent.data) as ChatCompletionChunk
const events = translateChunkToAnthropicEvents(chunk, streamState)
const chunk = JSON.parse(rawEvent.data) as ChatCompletionChunk
const events = translateChunkToAnthropicEvents(chunk, streamState)

for (const event of events) {
for (const event of events) {
if (consola.level >= 4) {
consola.debug("Translated Anthropic event:", JSON.stringify(event))
await stream.writeSSE({
event: event.type,
data: JSON.stringify(event),
})
}
await stream.writeSSE({
event: event.type,
data: JSON.stringify(event),
})
}
})
}
}

const isNonStreaming = (
Expand Down
8 changes: 8 additions & 0 deletions src/routes/messages/non-stream-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ function translateAnthropicMessagesToOpenAI(
: handleAssistantMessage(message),
)

const lastMessage = otherMessages.at(-1)
if (
lastMessage?.role === "assistant"
&& (!lastMessage.tool_calls || lastMessage.tool_calls.length === 0)
) {
otherMessages.push({ role: "user", content: "Continue." })
}

return [...systemMessages, ...otherMessages]
}

Expand Down
46 changes: 36 additions & 10 deletions src/services/copilot/create-chat-completions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import consola from "consola"
import { events } from "fetch-event-stream"
import { getGlobalDispatcher, type Dispatcher } from "undici"

import { copilotHeaders, copilotBaseUrl } from "~/lib/api-config"
import { HTTPError } from "~/lib/error"
import { state } from "~/lib/state"
import { refreshCopilotToken } from "~/lib/token"

export const createChatCompletions = async (
payload: ChatCompletionsPayload,
signal?: AbortSignal,
) => {
if (!state.copilotToken) throw new Error("Copilot token not found")

Expand All @@ -22,25 +25,48 @@ export const createChatCompletions = async (
["assistant", "tool"].includes(msg.role),
)

// Build headers and add X-Initiator
const headers: Record<string, string> = {
...copilotHeaders(state, enableVision),
"X-Initiator": isAgentCall ? "agent" : "user",
const body = JSON.stringify(payload)
const send = () => {
// Build headers (with the current Copilot token) and add X-Initiator
const headers: Record<string, string> = {
...copilotHeaders(state, enableVision),
"X-Initiator": isAgentCall ? "agent" : "user",
}

const requestInit: RequestInit & { dispatcher: Dispatcher } = {
method: "POST",
headers,
body,
signal,
dispatcher: getGlobalDispatcher(),
}
return fetch(`${copilotBaseUrl(state)}/chat/completions`, requestInit)
}

const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
method: "POST",
headers,
body: JSON.stringify(payload),
})
let response = await send()

// The Copilot token can be revoked server-side before its scheduled
// refresh. On 401/403, fetch a fresh token and retry once.
if (response.status === 401 || response.status === 403) {
consola.warn(
`Copilot returned ${response.status}, refreshing Copilot token and retrying`,
)
try {
await refreshCopilotToken()
await response.body?.cancel()
response = await send()
} catch (error) {
consola.error("Failed to refresh Copilot token:", error)
}
Comment on lines +54 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Limit the try/catch to refreshCopilotToken(). Let errors from the retried send() propagate.

The catch block also handles errors from response.body?.cancel() and from the retried send(). Here is the failure sequence:

  1. The refresh succeeds.
  2. Line 56 cancels the body of the first 401/403 response.
  3. The retried send() rejects. This happens on a network error, or with an AbortError when the client disconnects during the refresh.
  4. Line 59 logs "Failed to refresh Copilot token", which is wrong because the refresh succeeded.
  5. response still points to the first 401/403 response, whose body is already cancelled.
  6. Line 65 throws HTTPError with that response. A downstream error handler that reads error.response.text() or .json() then fails on the cancelled body.

The result is that the real error (network failure or abort) is lost. The client gets a misleading 401/403 or an error-handler failure. The route handlers check for error.name !== "AbortError" to skip logging aborts, so an AbortError swallowed here also gets logged as a token-refresh failure.

Catch only the refresh failure. If the refresh fails, keep the first response and do not cancel its body.

🐛 Proposed fix
--- "a/src/services/copilot/create-chat-completions.ts"
+++ "b/src/services/copilot/create-chat-completions.ts"
@@ -51,13 +51,17 @@
     consola.warn(
       `Copilot returned ${response.status}, refreshing Copilot token and retrying`,
     )
-    try {
-      await refreshCopilotToken()
-      await response.body?.cancel()
-      response = await send()
-    } catch (error) {
-      consola.error("Failed to refresh Copilot token:", error)
-    }
+    let refreshed = false
+    try {
+      await refreshCopilotToken()
+      refreshed = true
+    } catch (error) {
+      consola.error("Failed to refresh Copilot token:", error)
+    }
+    if (refreshed) {
+      await response.body?.cancel()
+      response = await send()
+    }
   }
 
   if (!response.ok) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await refreshCopilotToken()
await response.body?.cancel()
response = await send()
} catch (error) {
consola.error("Failed to refresh Copilot token:", error)
}
let refreshed = false
try {
await refreshCopilotToken()
refreshed = true
} catch (error) {
consola.error("Failed to refresh Copilot token:", error)
}
if (refreshed) {
await response.body?.cancel()
response = await send()
}

}

if (!response.ok) {
consola.error("Failed to create chat completions", response)
throw new HTTPError("Failed to create chat completions", response)
}

if (payload.stream) {
return events(response)
return events(response, signal)
}

return (await response.json()) as ChatCompletionResponse
Expand Down
Loading