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
99 changes: 99 additions & 0 deletions src/lib/auto-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import consola from "consola"

import type { BridgeConfig } from "~/lib/config"
import { runtimeState } from "~/lib/state"

interface AutoSessionResponse {
session_token: string
available_models?: Array<string>
expires_at?: number | string
}

const AUTO_MODE_BODY = { auto_mode: { model_hints: ["auto"] } }
const FALLBACK_REFRESH_SECONDS = 30 * 60

const parseExpiresAt = (value: number | string | undefined): number | undefined => {
if (typeof value === "number" && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const numeric = Number(value)
if (Number.isFinite(numeric)) {
return numeric
}
const parsed = Date.parse(value)
if (Number.isFinite(parsed)) {
return Math.floor(parsed / 1000)
}
}
return undefined
}

export const fetchAutoSession = async (
config: BridgeConfig,
): Promise<AutoSessionResponse> => {
if (!config.copilotToken) {
throw new Error(
"COPILOT_TOKEN is not configured; cannot start auto mode",
)
}

const response = await fetch(`${config.copilotBaseUrl}/models/session`, {
method: "POST",
headers: {
authorization: `Bearer ${config.copilotToken}`,
"content-type": "application/json",
"x-github-api-version": "2025-10-01",
"copilot-integration-id": "vscode-chat",
},
body: JSON.stringify(AUTO_MODE_BODY),
})

if (!response.ok) {
const text = await response.text().catch(() => "")
throw new Error(
`Failed to acquire auto-mode session token: ${response.status} ${response.statusText}\n${text}`,
)
}

return (await response.json()) as AutoSessionResponse
}

const applyAutoSession = async (config: BridgeConfig) => {
const data = await fetchAutoSession(config)
runtimeState.autoSessionToken = data.session_token
runtimeState.autoExpiresAt = parseExpiresAt(data.expires_at)
runtimeState.autoAvailableModels = data.available_models
return data
}

const scheduleAutoSessionRefresh = (config: BridgeConfig) => {
const now = Math.floor(Date.now() / 1000)
const expiresAt =
runtimeState.autoExpiresAt ?? now + FALLBACK_REFRESH_SECONDS
const refreshIn = Math.max(expiresAt - now - 60, 60)

const timer = setTimeout(async () => {
try {
await applyAutoSession(config)
consola.debug("Refreshed Copilot auto-mode session token")
} catch (error) {
consola.error("Failed to refresh auto-mode session token:", error)
} finally {
scheduleAutoSessionRefresh(config)
}
}, refreshIn * 1000)

if (typeof timer.unref === "function") {
timer.unref()
}
}

export const enableAutoMode = async (
config: BridgeConfig,
): Promise<AutoSessionResponse> => {
const data = await applyAutoSession(config)
runtimeState.autoMode = true
scheduleAutoSessionRefresh(config)
return data
}
4 changes: 4 additions & 0 deletions src/lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export interface RuntimeState {
rateLimitSeconds?: number
rateLimitWait?: boolean
lastRequestTimestamp?: number
autoMode?: boolean
autoSessionToken?: string
autoExpiresAt?: number
autoAvailableModels?: Array<string>
}

export const runtimeState: RuntimeState = {}
11 changes: 10 additions & 1 deletion src/providers/copilot/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { appendFile } from "node:fs/promises"

import type { BridgeConfig } from "~/lib/config"
import { BridgeNotImplementedError } from "~/lib/error"
import { runtimeState } from "~/lib/state"

const COPILOT_VERSION = "0.26.7"
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
const API_VERSION = "2025-04-01"
const AUTO_MODE_API_VERSION = "2025-10-01"
const MAX_FETCH_ATTEMPTS = 2

export interface CopilotProviderContext {
Expand Down Expand Up @@ -45,7 +47,10 @@ const buildHeaders = (
headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION)
headers.set("user-agent", USER_AGENT)
headers.set("openai-intent", "conversation-panel")
headers.set("x-github-api-version", API_VERSION)
headers.set(
"x-github-api-version",
runtimeState.autoSessionToken ? AUTO_MODE_API_VERSION : API_VERSION,
)
headers.set("x-request-id", randomUUID())
headers.set("x-vscode-user-agent-library-version", "electron-fetch")

Expand All @@ -57,6 +62,10 @@ const buildHeaders = (
headers.set("x-initiator", options.initiator)
}

if (runtimeState.autoSessionToken) {
headers.set("copilot-session-token", runtimeState.autoSessionToken)
}

if (!headers.has("content-type") && init.body !== undefined) {
headers.set("content-type", "application/json")
}
Expand Down
34 changes: 33 additions & 1 deletion src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineCommand } from "citty"
import consola from "consola"

import { setupBridgeAuth } from "~/lib/auth"
import { enableAutoMode } from "~/lib/auto-session"
import {
applyClaudeConfig,
parsePortFromBaseUrl,
Expand Down Expand Up @@ -116,6 +117,12 @@ export const start = defineCommand({
description:
"When --rate-limit is set, wait instead of returning HTTP 429.",
},
auto: {
type: "boolean",
default: false,
description:
"Acquire a Copilot auto-mode session token and attach it to every upstream request (bypasses the router intent step; only auto-mode models are reachable).",
},
},
async run({ args }) {
// Port resolution priority (high → low):
Expand Down Expand Up @@ -167,6 +174,22 @@ export const start = defineCommand({
showToken: args["show-token"],
})

if (args.auto) {
try {
const session = await enableAutoMode(config)
consola.success(
`Auto mode enabled${
session.available_models?.length ?
` (available models: ${session.available_models.join(", ")})`
: ""
}`,
)
} catch (error) {
consola.error("Failed to enable auto mode:", error)
process.exit(1)
}
}

const server = startServer(config)

consola.success(
Expand Down Expand Up @@ -293,7 +316,16 @@ export const start = defineCommand({
.map((id) => getPublicModelId(id)),
)
: fallbackModelIds
const finalPickable = pickable.length > 0 ? pickable : fallbackModelIds
const autoAllowed =
args.auto && runtimeState.autoAvailableModels?.length ?
new Set(runtimeState.autoAvailableModels.map(getPublicModelId))
: undefined
const autoFilteredPickable =
autoAllowed ? pickable.filter((id) => autoAllowed.has(id)) : pickable
const finalPickable =
autoFilteredPickable.length > 0 ? autoFilteredPickable
: pickable.length > 0 ? pickable
: fallbackModelIds
if (models.length > 0) {
consola.info(
`Available models:\n${models
Expand Down