Skip to content

feat: improve copilot token refresh resilience#213

Closed
benben17 wants to merge 4 commits intoericc-ch:masterfrom
benben17:pr/ericc-resilience
Closed

feat: improve copilot token refresh resilience#213
benben17 wants to merge 4 commits intoericc-ch:masterfrom
benben17:pr/ericc-resilience

Conversation

@benben17
Copy link
Copy Markdown

Key improvements:
> - Added network retry logic for token fetching.
> - Implemented exponential backoff for background token refresh.
> - Ensures usage of cached tokens if a refresh attempt fails but the current token is still valid.
>
> Note: Some files were added as they were not present in the base v0.7.0 version but are required for the enhanced token management.

Copilot AI review requested due to automatic review settings March 22, 2026 01:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to make GitHub Copilot token retrieval/refresh more resilient to transient network failures by adding retries/timeouts and introducing a token manager intended to centralize refresh behavior.

Changes:

  • Added retry + timeout + backoff behavior to GitHub Copilot token fetching.
  • Added a new CopilotTokenManager singleton with background refresh + retry scheduling and “use cached token if still valid” behavior.
  • Added a new Responses route handler (currently incomplete in this repo state due to missing dependencies).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.

File Description
src/services/github/get-copilot-token.ts Adds timeout + retry loop with exponential backoff around token fetch.
src/routes/responses/handler.ts Introduces a Responses endpoint handler (but currently references missing modules/files).
src/lib/copilot-token-manager.ts Adds a singleton token manager with auto-refresh, backoff retries, and fallback to cached token.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +38
export const handleResponses = async (c: Context) => {
await checkRateLimit(state)

const payload = await c.req.json<ResponsesPayload>()
logger.debug("Responses request payload:", JSON.stringify(payload))
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

logger.debug("Responses request payload:", JSON.stringify(payload)) logs the full user request body, which can include sensitive prompt data and can be very large. Consider redacting message content (or logging only metadata like model/tool names) and truncating to a safe size to avoid leaking data and blowing up logs.

Suggested change
export const handleResponses = async (c: Context) => {
await checkRateLimit(state)
const payload = await c.req.json<ResponsesPayload>()
logger.debug("Responses request payload:", JSON.stringify(payload))
const summarizePayloadForLog = (payload: ResponsesPayload) => {
return {
model: payload.model,
stream: (payload as { stream?: boolean }).stream,
max_output_tokens: (payload as { max_output_tokens?: number }).max_output_tokens,
tools:
payload.tools &&
payload.tools.map((tool) => {
const base = { type: tool.type }
if ("name" in tool && typeof (tool as ResponseTool & { name?: string }).name === "string") {
return { ...base, name: (tool as ResponseTool & { name?: string }).name }
}
return base
}),
messages_count:
Array.isArray((payload as { messages?: unknown[] }).messages)
? (payload as { messages?: unknown[] }).messages!.length
: undefined,
}
}
export const handleResponses = async (c: Context) => {
await checkRateLimit(state)
const payload = await c.req.json<ResponsesPayload>()
const safePayloadForLog = summarizePayloadForLog(payload)
logger.debug(
"Responses request payload (summary):",
JSON.stringify(safePayloadForLog),
)

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +165
// Export singleton instance
export const copilotTokenManager = new CopilotTokenManager()
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

copilotTokenManager is exported here but not referenced anywhere else in src/ (the server startup still calls setupCopilotToken() in src/lib/token.ts, which keeps its own setInterval refresh). If the intent of this PR is to improve refresh resilience globally, the manager needs to be integrated into the existing token setup / request paths, otherwise these new behaviors won’t take effect.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to 26
const isRetryableNetworkError = (error: unknown): boolean => {
if (!(error instanceof Error)) return false
const networkErrorCodes = new Set([
"ECONNRESET",
"ETIMEDOUT",
"EAI_AGAIN",
"ECONNREFUSED",
"ENOTFOUND",
])
const maybeCode = (error as Error & { code?: unknown }).code
const code = typeof maybeCode === "string" ? maybeCode : undefined

if (code && networkErrorCodes.has(code)) return true

return /socket connection was closed unexpectedly|network|fetch failed|timeout|timed out/i.test(
error.message,
)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

AbortController timeouts will throw an abort/timeout error, but isRetryableNetworkError doesn’t currently classify common abort errors (e.g. error.name === "AbortError" / message containing "aborted") as retryable. As a result, timeouts triggered by COPILOT_TOKEN_TIMEOUT_MS will typically not be retried even though the code intends to retry network failures.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +6
import { getConfig, getMappedModel } from "~/lib/config"
import { createHandlerLogger } from "~/lib/logger"
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

This file imports ~/lib/config, but that module does not exist in the current repo. With the default tsconfig.json (no include/exclude), TypeScript will typecheck this file and the build will fail until the missing module is added or the import is removed/updated.

Copilot uses AI. Check for mistakes.
import { streamSSE } from "hono/streaming"

import { getConfig, getMappedModel } from "~/lib/config"
import { createHandlerLogger } from "~/lib/logger"
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

This file imports ~/lib/logger, but that module does not exist in the current repo. This will cause TypeScript compilation to fail unless the module is added (or the handler is adjusted to use an existing logging utility).

Copilot uses AI. Check for mistakes.
benben17 and others added 3 commits March 22, 2026 11:48
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@caozhiyuan
Copy link
Copy Markdown
Contributor

@benben17 you can use this https://github.com/caozhiyuan/copilot-api (Includes many new features). These features were already included in the previous pull request, but this repository has not been maintained for a long time.

@benben17 benben17 closed this by deleting the head repository Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants