mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-06 12:24:33 +00:00
fix(core): classify GitHub Copilot requests on every route (#47160)
This commit is contained in:
parent
c5dca2df37
commit
ffac1c5b11
2 changed files with 102 additions and 20 deletions
|
|
@ -250,15 +250,26 @@ export const GithubCopilotPlugin = define({
|
|||
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
||||
}),
|
||||
)
|
||||
// Runs for every route, unlike http.request, which the AI SDK route bypasses.
|
||||
yield* ctx.session.hook(
|
||||
"model.request",
|
||||
(evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.model.providerID !== Provider.ID.githubCopilot) return
|
||||
const session = yield* ctx.session
|
||||
.get({ sessionID: evt.sessionID })
|
||||
.pipe(Effect.orElseSucceed(() => undefined))
|
||||
const interaction = interactionType(evt.agent, session?.parentID !== undefined)
|
||||
evt.headers["X-Interaction-Type"] = interaction
|
||||
if (interaction !== "conversation-agent") evt.headers["x-initiator"] = "agent"
|
||||
}),
|
||||
{ providerID: Provider.ID.githubCopilot },
|
||||
)
|
||||
yield* ctx.session.hook(
|
||||
"http.request",
|
||||
(evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.model.providerID !== Provider.ID.githubCopilot) return
|
||||
if (evt.agent === Agent.ID.make("title"))
|
||||
evt.request.headers.set("X-Interaction-Type", "conversation-background")
|
||||
if (evt.agent === Agent.ID.make("compaction"))
|
||||
evt.request.headers.set("X-Interaction-Type", "conversation-compaction")
|
||||
const token = evt.request.headers.get("x-api-key")
|
||||
if (!token) return
|
||||
const text = yield* Effect.promise(() => evt.request.clone().text())
|
||||
|
|
@ -370,11 +381,23 @@ function applyHeaders(
|
|||
headers.set("User-Agent", App.useragent(app))
|
||||
headers.set("Openai-Intent", "conversation-edits")
|
||||
headers.set("X-GitHub-Api-Version", apiVersion)
|
||||
headers.set("x-initiator", metadata.agent ? "agent" : "user")
|
||||
// The step may already have declared itself agent-initiated (subagent, title, compaction);
|
||||
// the body can only ever escalate to "agent", never back to "user".
|
||||
if (metadata.agent) headers.set("x-initiator", "agent")
|
||||
else if (!headers.has("x-initiator")) headers.set("x-initiator", "user")
|
||||
if (metadata.vision) headers.set("Copilot-Vision-Request", "true")
|
||||
if (anthropic) headers.set("anthropic-beta", "interleaved-thinking-2025-05-14")
|
||||
}
|
||||
|
||||
// Mirrors the Copilot client's X-Interaction-Type vocabulary: the agent loop is the default,
|
||||
// nested sessions are subagents, and title/compaction are the two utility overrides.
|
||||
export function interactionType(agent: Agent.ID, child: boolean) {
|
||||
if (agent === Agent.ID.make("title")) return "conversation-background"
|
||||
if (agent === Agent.ID.make("compaction")) return "conversation-compaction"
|
||||
if (child) return "conversation-subagent"
|
||||
return "conversation-agent"
|
||||
}
|
||||
|
||||
type RequestMetadata = ReturnType<typeof requestMetadata>
|
||||
|
||||
function requestMetadata(url: string, body: unknown) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { App } from "@opencode-ai/core/app"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
|
|
@ -35,6 +36,24 @@ function required<T>(value: T | undefined): T {
|
|||
return value
|
||||
}
|
||||
|
||||
const sessions = Effect.fn(function* () {
|
||||
const service = yield* Session.Service
|
||||
const location = yield* Location.Service
|
||||
const parent = yield* service.create({ location: { directory: location.directory } })
|
||||
const child = yield* service.create({ parentID: parent.id })
|
||||
return { parent: parent.id, child: child.id }
|
||||
})
|
||||
|
||||
const modelRequest = Effect.fn(function* (sessionID: Session.ID, agent: string) {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
return yield* hooks.trigger("session", "model.request", {
|
||||
sessionID,
|
||||
agent: Agent.ID.make(agent),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.githubCopilot, id: Model.ID.make("gpt-5.4") }),
|
||||
headers: {},
|
||||
})
|
||||
})
|
||||
|
||||
describe("GithubCopilotPlugin", () => {
|
||||
test("prefers the account-specific Copilot API endpoint", () => {
|
||||
expect(
|
||||
|
|
@ -149,31 +168,71 @@ describe("GithubCopilotPlugin", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies main-loop steps as agent interactions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
const event = yield* modelRequest((yield* sessions()).parent, "build")
|
||||
expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-agent" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies child-session steps as subagent interactions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
const event = yield* modelRequest((yield* sessions()).child, "build")
|
||||
expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-subagent", "x-initiator": "agent" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies title generation as a background interaction", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const event = yield* hooks.trigger("session", "http.request", {
|
||||
sessionID: Session.ID.make("ses_title"),
|
||||
agent: Agent.ID.make("title"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.githubCopilot, id: Model.ID.make("gpt-5.4-nano") }),
|
||||
request: new Request("https://api.githubcopilot.com/chat/completions"),
|
||||
})
|
||||
expect(event.request.headers.get("x-interaction-type")).toBe("conversation-background")
|
||||
const event = yield* modelRequest((yield* sessions()).parent, "title")
|
||||
expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-background", "x-initiator": "agent" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies compaction requests", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
const event = yield* modelRequest((yield* sessions()).child, "compaction")
|
||||
expect(event.headers).toEqual({ "X-Interaction-Type": "conversation-compaction", "x-initiator": "agent" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("ignores other providers' model requests", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const event = yield* hooks.trigger("session", "http.request", {
|
||||
sessionID: Session.ID.make("ses_compaction"),
|
||||
agent: Agent.ID.make("compaction"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.githubCopilot, id: Model.ID.make("gpt-5.4") }),
|
||||
request: new Request("https://api.githubcopilot.com/responses"),
|
||||
const event = yield* hooks.trigger("session", "model.request", {
|
||||
sessionID: (yield* sessions()).parent,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("openai"), id: Model.ID.make("gpt-5.4") }),
|
||||
headers: {},
|
||||
})
|
||||
expect(event.request.headers.get("x-interaction-type")).toBe("conversation-compaction")
|
||||
expect(event.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("keeps a declared agent initiator when the body looks user-initiated", () =>
|
||||
Effect.gen(function* () {
|
||||
const requests: Headers[] = []
|
||||
const send = copilotFetch(
|
||||
"token",
|
||||
async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
requests.push(new Headers(init?.headers))
|
||||
return Response.json({ ok: true })
|
||||
},
|
||||
App.make({ name: "test", version: "1.2.3", channel: "beta" }),
|
||||
)
|
||||
yield* Effect.promise(() =>
|
||||
send("https://api.githubcopilot.com/chat/completions", {
|
||||
method: "POST",
|
||||
headers: { "x-initiator": "agent" },
|
||||
body: JSON.stringify({ messages: [{ role: "user", content: "summarize" }] }),
|
||||
}),
|
||||
)
|
||||
expect(requests[0]?.get("x-initiator")).toBe("agent")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue