Skip to content

Commit 6ae4b07

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 6ae4b07

14 files changed

Lines changed: 306 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: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { randomUUID } from "node:crypto"
2-
31
import type { State } from "./state"
42

53
export const standardHeaders = () => ({
64
"content-type": "application/json",
75
accept: "application/json",
86
})
97

10-
const COPILOT_VERSION = "0.37.6"
8+
const COPILOT_VERSION = "0.38.2"
119
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`
1210
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
1311

@@ -17,7 +15,11 @@ export const copilotBaseUrl = (state: State) =>
1715
state.accountType === "individual" ?
1816
"https://api.githubcopilot.com"
1917
: `https://api.${state.accountType}.githubcopilot.com`
20-
export const copilotHeaders = (state: State, vision: boolean = false) => {
18+
export const copilotHeaders = (
19+
state: State,
20+
requestId: string,
21+
vision: boolean = false,
22+
) => {
2123
const headers: Record<string, string> = {
2224
Authorization: `Bearer ${state.copilotToken}`,
2325
"content-type": standardHeaders()["content-type"],
@@ -27,12 +29,22 @@ export const copilotHeaders = (state: State, vision: boolean = false) => {
2729
"user-agent": USER_AGENT,
2830
"openai-intent": "conversation-agent",
2931
"x-github-api-version": API_VERSION,
30-
"x-request-id": randomUUID(),
32+
"x-request-id": requestId,
3133
"x-vscode-user-agent-library-version": "electron-fetch",
34+
"x-agent-task-id": requestId,
35+
"x-interaction-type": "conversation-agent",
3236
}
3337

3438
if (vision) headers["copilot-vision-request"] = "true"
3539

40+
if (state.macMachineId) {
41+
headers["vscode-machineid"] = state.macMachineId
42+
}
43+
44+
if (state.vsCodeSessionId) {
45+
headers["vscode-sessionid"] = state.vsCodeSessionId
46+
}
47+
3648
return headers
3749
}
3850

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: 112 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,109 @@ 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+
export const cacheVsCodeSessionId = () => {
70+
state.vsCodeSessionId = randomUUID() + Date.now().toString()
71+
consola.debug(`Generated VSCode session ID: ${state.vsCodeSessionId}`)
72+
}
73+
74+
interface PayloadMessage {
75+
role?: string
76+
content?: string | Array<{ type?: string; text?: string }> | null
77+
type?: string
78+
}
79+
80+
const findLastUserContent = (
81+
messages: Array<PayloadMessage>,
82+
): string | null => {
83+
for (let i = messages.length - 1; i >= 0; i--) {
84+
const msg = messages[i]
85+
if (msg.role === "user" && msg.content) {
86+
if (typeof msg.content === "string") {
87+
return msg.content
88+
} else if (Array.isArray(msg.content)) {
89+
const array = msg.content.filter((n) => n.type !== "tool_result")
90+
if (array.length > 0) {
91+
return JSON.stringify(array)
92+
}
93+
}
94+
}
95+
}
96+
return null
97+
}
98+
99+
export const generateRequestIdFromPayload = (payload: {
100+
messages: string | Array<PayloadMessage> | undefined
101+
}): string => {
102+
const messages = payload.messages
103+
if (messages) {
104+
const lastUserContent =
105+
typeof messages === "string" ? messages : findLastUserContent(messages)
106+
107+
if (lastUserContent) {
108+
return getUUID(lastUserContent)
109+
}
110+
}
111+
112+
return randomUUID()
113+
}
114+
115+
export const getRootSessionId = (
116+
anthropicPayload: AnthropicMessagesPayload,
117+
c: Context,
118+
): string | undefined => {
119+
let sessionId: string | undefined
120+
if (anthropicPayload.metadata?.user_id) {
121+
const sessionMatch = new RegExp(/_session_(.+)$/).exec(
122+
anthropicPayload.metadata.user_id,
123+
)
124+
sessionId = sessionMatch ? sessionMatch[1] : undefined
125+
} else {
126+
sessionId = c.req.header("x-session-id")
127+
}
128+
if (sessionId) {
129+
return getUUID(sessionId)
130+
}
131+
return sessionId
132+
}
133+
134+
const getUUID = (content: string): string => {
135+
const hash = createHash("sha256").update(content).digest("hex")
136+
const hash32 = hash.slice(0, 32)
137+
return `${hash32.slice(0, 8)}-${hash32.slice(8, 12)}-${hash32.slice(12, 16)}-${hash32.slice(16, 20)}-${hash32.slice(20)}`
138+
}

src/routes/chat-completions/handler.ts

Lines changed: 3 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, isNullish } from "~/lib/utils"
1111
import {
1212
createChatCompletions,
1313
type ChatCompletionResponse,
@@ -49,7 +49,8 @@ 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+
const requestId = generateRequestIdFromPayload(payload)
53+
const response = await createChatCompletions(payload, { requestId })
5354

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

0 commit comments

Comments
 (0)