Skip to content

Commit e69e6a8

Browse files
committed
feat: align with the request headers in the copilot extension version 0.38.2 and update opencode plugin to set header x-session-id
1 parent c9686a2 commit e69e6a8

16 files changed

Lines changed: 369 additions & 54 deletions

File tree

.opencode/plugins/subagent-marker.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const MARKER_PREFIX = "__SUBAGENT_MARKER__"
22

33
const subagentSessions = new Set()
44
const markedSessions = new Set()
5+
const sessionParentMap = new Map()
56

67
const getSessionInfo = (event) => {
78
if (!event || typeof event !== "object") return undefined
@@ -17,8 +18,13 @@ export const SubagentMarkerPlugin = async () => {
1718
event: async ({ event }) => {
1819
if (event.type === "session.created") {
1920
const info = getSessionInfo(event)
20-
if (info?.id && info.parentID) {
21-
subagentSessions.add(info.id)
21+
if (info?.id) {
22+
if (info.parentID) {
23+
subagentSessions.add(info.id)
24+
sessionParentMap.set(info.id, info.parentID)
25+
} else {
26+
sessionParentMap.set(info.id, info.id)
27+
}
2228
}
2329
return
2430
}
@@ -28,6 +34,7 @@ export const SubagentMarkerPlugin = async () => {
2834
if (info?.id) {
2935
subagentSessions.delete(info.id)
3036
markedSessions.delete(info.id)
37+
sessionParentMap.delete(info.id)
3138
}
3239
}
3340
},
@@ -58,8 +65,14 @@ export const SubagentMarkerPlugin = async () => {
5865
end: Date.now(),
5966
},
6067
})
61-
6268
markedSessions.add(sessionID)
6369
},
70+
"chat.headers": async (input, output) => {
71+
const { sessionID } = input
72+
const sessionIdValue = sessionParentMap.get(sessionID)
73+
if (sessionIdValue) {
74+
output.headers["x-session-id"] = sessionIdValue
75+
}
76+
},
6477
}
6578
}

README.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ You can also read more about IDE integration here: [Add Claude Code to your IDE]
377377

378378
### Subagent Marker Integration (Optional)
379379

380-
This project supports `X-Initiator: agent` for subagent-originated requests.
380+
This project supports `x-initiator: agent` for subagent-originated requests.
381381

382382
#### Claude Code plugin producer (marketplace-based)
383383

@@ -398,15 +398,31 @@ Install the plugin from the marketplace:
398398
/plugin install claude-plugin@copilot-api-marketplace
399399
```
400400

401-
After installation, the plugin injects `__SUBAGENT_MARKER__...` on `SubagentStart`, and this proxy uses it to infer `X-Initiator: agent`.
401+
After installation, the plugin injects `__SUBAGENT_MARKER__...` on `SubagentStart`, and this proxy uses it to infer `x-initiator: agent`.
402402

403403
#### Opencode plugin producer
404404

405-
For opencode, use the plugin implementation at:
405+
The marker producer is packaged as an opencode plugin located at `.opencode/plugins/subagent-marker.js`.
406406

407-
- `.opencode/plugins/subagent-marker.js`
407+
**Installation:**
408408

409-
This plugin tracks sub-sessions and prepends a marker system reminder to subagent chat messages.
409+
Copy the plugin file to your opencode plugins directory:
410+
411+
```sh
412+
# Clone or download this repository, then copy the plugin
413+
cp .opencode/plugins/subagent-marker.js ~/.config/opencode/plugins/
414+
```
415+
416+
Or manually create the file at `~/.config/opencode/plugins/subagent-marker.js` with the plugin content.
417+
418+
**Features:**
419+
420+
- Tracks sub-sessions created by subagents
421+
- Automatically prepends a marker system reminder (`__SUBAGENT_MARKER__...`) to subagent chat messages
422+
- Sets `x-session-id` header for session tracking
423+
- Enables this proxy to infer `x-initiator: agent` for subagent-originated requests
424+
425+
The plugin hooks into `session.created`, `session.deleted`, `chat.message`, and `chat.headers` events to provide seamless subagent marker functionality.
410426

411427
## Running from Source
412428

src/lib/api-config.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const standardHeaders = () => ({
77
accept: "application/json",
88
})
99

10-
const COPILOT_VERSION = "0.37.6"
10+
const COPILOT_VERSION = "0.38.2"
1111
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`
1212
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
1313

@@ -17,7 +17,12 @@ export const copilotBaseUrl = (state: State) =>
1717
state.accountType === "individual" ?
1818
"https://api.githubcopilot.com"
1919
: `https://api.${state.accountType}.githubcopilot.com`
20-
export const copilotHeaders = (state: State, vision: boolean = false) => {
20+
export const copilotHeaders = (
21+
state: State,
22+
requestId?: string,
23+
vision: boolean = false,
24+
) => {
25+
const requestIdValue = requestId ?? randomUUID()
2126
const headers: Record<string, string> = {
2227
Authorization: `Bearer ${state.copilotToken}`,
2328
"content-type": standardHeaders()["content-type"],
@@ -27,12 +32,22 @@ export const copilotHeaders = (state: State, vision: boolean = false) => {
2732
"user-agent": USER_AGENT,
2833
"openai-intent": "conversation-agent",
2934
"x-github-api-version": API_VERSION,
30-
"x-request-id": randomUUID(),
35+
"x-request-id": requestIdValue,
3136
"x-vscode-user-agent-library-version": "electron-fetch",
37+
"x-agent-task-id": requestIdValue,
38+
"x-interaction-type": "conversation-agent",
3239
}
3340

3441
if (vision) headers["copilot-vision-request"] = "true"
3542

43+
if (state.macMachineId) {
44+
headers["vscode-machineid"] = state.macMachineId
45+
}
46+
47+
if (state.vsCodeSessionId) {
48+
headers["vscode-sessionid"] = state.vsCodeSessionId
49+
}
50+
3651
return headers
3752
}
3853

src/lib/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const defaultConfig: AppConfig = {
5555
"gpt-5.4": gpt5CommentaryPrompt,
5656
},
5757
smallModel: "gpt-5-mini",
58-
responsesApiContextManagementModels: ["gpt-5.4", "gpt-5.3-codex"],
58+
responsesApiContextManagementModels: [],
5959
modelReasoningEfforts: {
6060
"gpt-5-mini": "low",
6161
"gpt-5.3-codex": "xhigh",

src/lib/state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export interface State {
88
models?: ModelsResponse
99
vsCodeVersion?: string
1010

11+
macMachineId?: string
12+
vsCodeSessionId?: string
13+
1114
manualApprove: boolean
1215
rateLimitWait: boolean
1316
showToken: boolean

src/lib/utils.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
import type { Context } from "hono"
2+
13
import consola from "consola"
4+
import { createHash, randomUUID } from "node:crypto"
5+
import { networkInterfaces } from "node:os"
6+
7+
import type { AnthropicMessagesPayload } from "~/routes/messages/anthropic-types"
28

39
import { getModels } from "~/services/copilot/get-models"
410
import { getVSCodeVersion } from "~/services/get-vscode-version"
@@ -24,3 +30,154 @@ export const cacheVSCodeVersion = async () => {
2430

2531
consola.info(`Using VSCode version: ${response}`)
2632
}
33+
34+
const invalidMacAddresses = new Set([
35+
"00:00:00:00:00:00",
36+
"ff:ff:ff:ff:ff:ff",
37+
"ac:de:48:00:11:22",
38+
])
39+
40+
function validateMacAddress(candidate: string): boolean {
41+
const tempCandidate = candidate.replaceAll("-", ":").toLowerCase()
42+
return !invalidMacAddresses.has(tempCandidate)
43+
}
44+
45+
export function getMac(): string | null {
46+
const ifaces = networkInterfaces()
47+
// eslint-disable-next-line guard-for-in
48+
for (const name in ifaces) {
49+
const networkInterface = ifaces[name]
50+
if (networkInterface) {
51+
for (const { mac } of networkInterface) {
52+
if (validateMacAddress(mac)) {
53+
return mac
54+
}
55+
}
56+
}
57+
}
58+
return null
59+
}
60+
61+
export const cacheMacMachineId = () => {
62+
const macAddress = getMac() ?? randomUUID()
63+
state.macMachineId = createHash("sha256")
64+
.update(macAddress, "utf8")
65+
.digest("hex")
66+
consola.debug(`Using machine ID: ${state.macMachineId}`)
67+
}
68+
69+
const SESSION_REFRESH_BASE_MS = 60 * 60 * 1000
70+
const SESSION_REFRESH_JITTER_MS = 20 * 60 * 1000
71+
let vsCodeSessionRefreshTimer: ReturnType<typeof setTimeout> | null = null
72+
73+
const generateSessionId = () => {
74+
state.vsCodeSessionId = randomUUID() + Date.now().toString()
75+
consola.debug(`Generated VSCode session ID: ${state.vsCodeSessionId}`)
76+
}
77+
78+
export const stopVsCodeSessionRefreshLoop = () => {
79+
if (vsCodeSessionRefreshTimer) {
80+
clearTimeout(vsCodeSessionRefreshTimer)
81+
vsCodeSessionRefreshTimer = null
82+
}
83+
}
84+
85+
const scheduleSessionIdRefresh = () => {
86+
const randomDelay = Math.floor(Math.random() * SESSION_REFRESH_JITTER_MS)
87+
const delay = SESSION_REFRESH_BASE_MS + randomDelay
88+
consola.debug(
89+
`Scheduling next VSCode session ID refresh in ${Math.round(
90+
delay / 1000,
91+
)} seconds`,
92+
)
93+
94+
stopVsCodeSessionRefreshLoop()
95+
vsCodeSessionRefreshTimer = setTimeout(() => {
96+
try {
97+
generateSessionId()
98+
} catch (error) {
99+
consola.error("Failed to refresh session ID, rescheduling...", error)
100+
} finally {
101+
scheduleSessionIdRefresh()
102+
}
103+
}, delay)
104+
}
105+
106+
export const cacheVsCodeSessionId = () => {
107+
stopVsCodeSessionRefreshLoop()
108+
generateSessionId()
109+
scheduleSessionIdRefresh()
110+
}
111+
112+
interface PayloadMessage {
113+
role?: string
114+
content?: string | Array<{ type?: string; text?: string }> | null
115+
type?: string
116+
}
117+
118+
const findLastUserContent = (
119+
messages: Array<PayloadMessage>,
120+
): string | null => {
121+
for (let i = messages.length - 1; i >= 0; i--) {
122+
const msg = messages[i]
123+
if (msg.role === "user" && msg.content) {
124+
if (typeof msg.content === "string") {
125+
return msg.content
126+
} else if (Array.isArray(msg.content)) {
127+
const array = msg.content
128+
.filter((n) => n.type !== "tool_result")
129+
.map((n) => ({ ...n, cache_control: undefined }))
130+
if (array.length > 0) {
131+
return JSON.stringify(array)
132+
}
133+
}
134+
}
135+
}
136+
return null
137+
}
138+
139+
export const generateRequestIdFromPayload = (
140+
payload: {
141+
messages: string | Array<PayloadMessage> | undefined
142+
},
143+
sessionId?: string,
144+
): string => {
145+
const messages = payload.messages
146+
if (messages) {
147+
const lastUserContent =
148+
typeof messages === "string" ? messages : findLastUserContent(messages)
149+
150+
if (lastUserContent) {
151+
return getUUID(
152+
(sessionId ?? "") + (state.macMachineId ?? "") + lastUserContent,
153+
)
154+
}
155+
}
156+
157+
return randomUUID()
158+
}
159+
160+
export const getRootSessionId = (
161+
anthropicPayload: AnthropicMessagesPayload,
162+
c: Context,
163+
): string | undefined => {
164+
let sessionId: string | undefined
165+
if (anthropicPayload.metadata?.user_id) {
166+
const sessionMatch = new RegExp(/_session_(.+)$/).exec(
167+
anthropicPayload.metadata.user_id,
168+
)
169+
sessionId = sessionMatch ? sessionMatch[1] : undefined
170+
} else {
171+
sessionId = c.req.header("x-session-id")
172+
}
173+
if (sessionId) {
174+
return getUUID(sessionId)
175+
}
176+
return sessionId
177+
}
178+
179+
export const getUUID = (content: string): string => {
180+
const hash = createHash("sha256").update(content).digest("hex")
181+
const hash32 = hash.slice(0, 32)
182+
return `${hash32.slice(0, 8)}-${hash32.slice(8, 12)}-${hash32.slice(12, 16)}-${hash32.slice(16, 20)}-${hash32.slice(20)}`
183+
}

src/routes/chat-completions/handler.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createHandlerLogger } from "~/lib/logger"
77
import { checkRateLimit } from "~/lib/rate-limit"
88
import { state } from "~/lib/state"
99
import { getTokenCount } from "~/lib/tokenizer"
10-
import { isNullish } from "~/lib/utils"
10+
import { generateRequestIdFromPayload, getUUID, isNullish } from "~/lib/utils"
1111
import {
1212
createChatCompletions,
1313
type ChatCompletionResponse,
@@ -49,7 +49,17 @@ export async function handleCompletion(c: Context) {
4949
logger.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens))
5050
}
5151

52-
const response = await createChatCompletions(payload)
52+
// not support subagent marker for now , set sessionId = getUUID(requestId)
53+
const requestId = generateRequestIdFromPayload(payload)
54+
logger.debug("Generated request ID:", requestId)
55+
56+
const sessionId = getUUID(requestId)
57+
logger.debug("Extracted session ID:", sessionId)
58+
59+
const response = await createChatCompletions(payload, {
60+
requestId,
61+
sessionId,
62+
})
5363

5464
if (isNonStreaming(response)) {
5565
logger.debug("Non-streaming response:", JSON.stringify(response))

0 commit comments

Comments
 (0)