Skip to content

Commit 4e673a6

Browse files
committed
feat: add --auto flag to bypass the auto-selection routing and provide an auto-available model when the planned models' quota is exhausted.
1 parent 2ea3753 commit 4e673a6

4 files changed

Lines changed: 146 additions & 2 deletions

File tree

src/lib/auto-session.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import consola from "consola"
2+
3+
import type { BridgeConfig } from "~/lib/config"
4+
import { runtimeState } from "~/lib/state"
5+
6+
interface AutoSessionResponse {
7+
session_token: string
8+
available_models?: Array<string>
9+
expires_at?: number | string
10+
}
11+
12+
const AUTO_MODE_BODY = { auto_mode: { model_hints: ["auto"] } }
13+
const FALLBACK_REFRESH_SECONDS = 30 * 60
14+
15+
const parseExpiresAt = (value: number | string | undefined): number | undefined => {
16+
if (typeof value === "number" && Number.isFinite(value)) {
17+
return value
18+
}
19+
if (typeof value === "string") {
20+
const numeric = Number(value)
21+
if (Number.isFinite(numeric)) {
22+
return numeric
23+
}
24+
const parsed = Date.parse(value)
25+
if (Number.isFinite(parsed)) {
26+
return Math.floor(parsed / 1000)
27+
}
28+
}
29+
return undefined
30+
}
31+
32+
export const fetchAutoSession = async (
33+
config: BridgeConfig,
34+
): Promise<AutoSessionResponse> => {
35+
if (!config.copilotToken) {
36+
throw new Error(
37+
"COPILOT_TOKEN is not configured; cannot start auto mode",
38+
)
39+
}
40+
41+
const response = await fetch(`${config.copilotBaseUrl}/models/session`, {
42+
method: "POST",
43+
headers: {
44+
authorization: `Bearer ${config.copilotToken}`,
45+
"content-type": "application/json",
46+
"x-github-api-version": "2025-10-01",
47+
"copilot-integration-id": "vscode-chat",
48+
},
49+
body: JSON.stringify(AUTO_MODE_BODY),
50+
})
51+
52+
if (!response.ok) {
53+
const text = await response.text().catch(() => "")
54+
throw new Error(
55+
`Failed to acquire auto-mode session token: ${response.status} ${response.statusText}\n${text}`,
56+
)
57+
}
58+
59+
return (await response.json()) as AutoSessionResponse
60+
}
61+
62+
const applyAutoSession = async (config: BridgeConfig) => {
63+
const data = await fetchAutoSession(config)
64+
runtimeState.autoSessionToken = data.session_token
65+
runtimeState.autoExpiresAt = parseExpiresAt(data.expires_at)
66+
runtimeState.autoAvailableModels = data.available_models
67+
return data
68+
}
69+
70+
const scheduleAutoSessionRefresh = (config: BridgeConfig) => {
71+
const now = Math.floor(Date.now() / 1000)
72+
const expiresAt =
73+
runtimeState.autoExpiresAt ?? now + FALLBACK_REFRESH_SECONDS
74+
const refreshIn = Math.max(expiresAt - now - 60, 60)
75+
76+
const timer = setTimeout(async () => {
77+
try {
78+
await applyAutoSession(config)
79+
consola.debug("Refreshed Copilot auto-mode session token")
80+
} catch (error) {
81+
consola.error("Failed to refresh auto-mode session token:", error)
82+
} finally {
83+
scheduleAutoSessionRefresh(config)
84+
}
85+
}, refreshIn * 1000)
86+
87+
if (typeof timer.unref === "function") {
88+
timer.unref()
89+
}
90+
}
91+
92+
export const enableAutoMode = async (
93+
config: BridgeConfig,
94+
): Promise<AutoSessionResponse> => {
95+
const data = await applyAutoSession(config)
96+
runtimeState.autoMode = true
97+
scheduleAutoSessionRefresh(config)
98+
return data
99+
}

src/lib/state.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export interface RuntimeState {
77
rateLimitSeconds?: number
88
rateLimitWait?: boolean
99
lastRequestTimestamp?: number
10+
autoMode?: boolean
11+
autoSessionToken?: string
12+
autoExpiresAt?: number
13+
autoAvailableModels?: Array<string>
1014
}
1115

1216
export const runtimeState: RuntimeState = {}

src/providers/copilot/client.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { appendFile } from "node:fs/promises"
33

44
import type { BridgeConfig } from "~/lib/config"
55
import { BridgeNotImplementedError } from "~/lib/error"
6+
import { runtimeState } from "~/lib/state"
67

78
const COPILOT_VERSION = "0.26.7"
89
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`
910
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
1011
const API_VERSION = "2025-04-01"
12+
const AUTO_MODE_API_VERSION = "2025-10-01"
1113
const MAX_FETCH_ATTEMPTS = 2
1214

1315
export interface CopilotProviderContext {
@@ -45,7 +47,10 @@ const buildHeaders = (
4547
headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION)
4648
headers.set("user-agent", USER_AGENT)
4749
headers.set("openai-intent", "conversation-panel")
48-
headers.set("x-github-api-version", API_VERSION)
50+
headers.set(
51+
"x-github-api-version",
52+
runtimeState.autoSessionToken ? AUTO_MODE_API_VERSION : API_VERSION,
53+
)
4954
headers.set("x-request-id", randomUUID())
5055
headers.set("x-vscode-user-agent-library-version", "electron-fetch")
5156

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

65+
if (runtimeState.autoSessionToken) {
66+
headers.set("copilot-session-token", runtimeState.autoSessionToken)
67+
}
68+
6069
if (!headers.has("content-type") && init.body !== undefined) {
6170
headers.set("content-type", "application/json")
6271
}

src/start.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineCommand } from "citty"
22
import consola from "consola"
33

44
import { setupBridgeAuth } from "~/lib/auth"
5+
import { enableAutoMode } from "~/lib/auto-session"
56
import {
67
applyClaudeConfig,
78
parsePortFromBaseUrl,
@@ -139,6 +140,12 @@ export const start = defineCommand({
139140
description:
140141
"When --rate-limit is set, wait instead of returning HTTP 429.",
141142
},
143+
auto: {
144+
type: "boolean",
145+
default: false,
146+
description:
147+
"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).",
148+
},
142149
},
143150
async run({ args }) {
144151
// Port resolution priority (high → low):
@@ -190,6 +197,22 @@ export const start = defineCommand({
190197
showToken: args["show-token"],
191198
})
192199

200+
if (args.auto) {
201+
try {
202+
const session = await enableAutoMode(config)
203+
consola.success(
204+
`Auto mode enabled${
205+
session.available_models?.length ?
206+
` (available models: ${session.available_models.join(", ")})`
207+
: ""
208+
}`,
209+
)
210+
} catch (error) {
211+
consola.error("Failed to enable auto mode:", error)
212+
process.exit(1)
213+
}
214+
}
215+
193216
const server = startServer(config)
194217

195218
consola.info(`copilot-bridge version: ${BRIDGE_VERSION}`)
@@ -326,7 +349,16 @@ export const start = defineCommand({
326349
.map((id) => getPublicModelId(id)),
327350
)
328351
: fallbackModelIds
329-
const finalPickable = pickable.length > 0 ? pickable : fallbackModelIds
352+
const autoAllowed =
353+
args.auto && runtimeState.autoAvailableModels?.length ?
354+
new Set(runtimeState.autoAvailableModels.map(getPublicModelId))
355+
: undefined
356+
const autoFilteredPickable =
357+
autoAllowed ? pickable.filter((id) => autoAllowed.has(id)) : pickable
358+
const finalPickable =
359+
autoFilteredPickable.length > 0 ? autoFilteredPickable
360+
: pickable.length > 0 ? pickable
361+
: fallbackModelIds
330362
if (models.length > 0) {
331363
consola.info(
332364
`Available models:\n${models

0 commit comments

Comments
 (0)