Compare commits

...

1 Commits

Author SHA1 Message Date
Aiden Cline ed38466a9d feat(core): use copilot utility models for titles 2026-08-21 00:36:43 -05:00
2 changed files with 123 additions and 2 deletions
@@ -148,6 +148,7 @@ const oauth = (app: App.Info) =>
export const GithubCopilotPlugin = define({
id: "opencode.provider.github.copilot",
effect: Effect.fn(function* (ctx) {
const agents = yield* Agent.Service
const catalog = yield* Catalog.Service
const bus = yield* Bus.Service
const loading = Semaphore.makeUnsafe(1)
@@ -246,8 +247,14 @@ export const GithubCopilotPlugin = define({
(evt) =>
Effect.gen(function* () {
if (evt.model.providerID !== Provider.ID.githubCopilot) return
if (evt.agent === Agent.ID.make("title"))
if (evt.agent === Agent.ID.make("title")) {
evt.request.headers.set("X-Interaction-Type", "conversation-background")
// Auto-selected title models come from the regular catalog and consume
// premium request quota, so reroute them to a utility model unless the
// user explicitly configured a title model.
if (!(yield* agents.get(Agent.ID.make("title")))?.model)
evt.request = (yield* utilityTitleRequest(catalog, evt.request)) ?? evt.request
}
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")
@@ -283,6 +290,32 @@ export const GithubCopilotPlugin = define({
}),
} satisfies PluginInternal.InternalPlugin)
// GitHub exposes utility models for background work such as title generation
// without including them in the model picker or premium request quota.
const utilityModels = ["gpt-5.4-nano", "gpt-4.1", "gpt-4o", "gpt-4o-mini"].map((id) => Model.ID.make(id))
const utilityTitleRequest = Effect.fn("GithubCopilotPlugin.utilityTitleRequest")(function* (
catalog: Catalog.Interface,
request: Request,
) {
const endpoint = request.url.includes("/chat/completions")
? "chat"
: request.url.includes("/responses")
? "responses"
: undefined
if (!endpoint) return
const target = (yield* Effect.forEach(utilityModels, (id) => catalog.model.get(Provider.ID.githubCopilot, id)))
.filter((model) => model !== undefined)
.find((model) => (model.settings?.endpoint ?? "chat") === endpoint)
if (!target) return
const body = Option.getOrUndefined(decodeBody(yield* Effect.promise(() => request.clone().text())))
if (!record(body) || typeof body.model !== "string" || body.model === target.modelID) return
const rewritten = new Request(request, { body: JSON.stringify({ ...body, model: target.modelID }) })
// The stale length would no longer match the rewritten body.
rewritten.headers.delete("content-length")
return rewritten
})
function normalizeDomain(input: string) {
return input.replace(/^https?:\/\//, "").replace(/\/$/, "")
}
@@ -1,6 +1,6 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { App } from "@opencode-ai/core/app"
import { Agent } from "@opencode-ai/schema/agent"
import { Agent } from "@opencode-ai/core/agent"
import { Session } from "@opencode-ai/schema/session"
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
@@ -154,6 +154,94 @@ describe("GithubCopilotPlugin", () => {
}),
)
it.effect("reroutes auto-selected title requests to a utility model", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* addPlugin()
yield* catalog.transform((evt) => {
evt.model.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.4-nano"), (model) => {
model.enabled = false
model.settings = { endpoint: "responses" }
})
})
const event = yield* (yield* PluginHooks.Service).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.6-luna") }),
request: new Request("https://api.githubcopilot.com/responses", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "gpt-5.6-luna", input: [{ role: "user", content: "hello" }] }),
}),
})
expect(yield* Effect.promise(() => event.request.clone().json())).toEqual({
model: "gpt-5.4-nano",
input: [{ role: "user", content: "hello" }],
})
expect(event.request.headers.get("x-interaction-type")).toBe("conversation-background")
}),
)
it.effect("keeps an explicitly configured title model", () =>
Effect.gen(function* () {
const agents = yield* Agent.Service
const catalog = yield* Catalog.Service
yield* addPlugin()
yield* agents.transform((draft) => {
draft.update(Agent.ID.make("title"), (agent) => {
agent.model = Model.Ref.make({ providerID: Provider.ID.githubCopilot, id: Model.ID.make("gpt-5.6-luna") })
})
})
yield* catalog.transform((evt) => {
evt.model.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.4-nano"), (model) => {
model.enabled = false
model.settings = { endpoint: "responses" }
})
})
const event = yield* (yield* PluginHooks.Service).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.6-luna") }),
request: new Request("https://api.githubcopilot.com/responses", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "gpt-5.6-luna", input: [{ role: "user", content: "hello" }] }),
}),
})
expect(yield* Effect.promise(() => event.request.clone().json())).toEqual({
model: "gpt-5.6-luna",
input: [{ role: "user", content: "hello" }],
})
}),
)
it.effect("keeps title requests on an endpoint without a utility model", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* addPlugin()
yield* catalog.transform((evt) => {
evt.model.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.4-nano"), (model) => {
model.enabled = false
model.settings = { endpoint: "responses" }
})
})
const event = yield* (yield* PluginHooks.Service).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("claude-haiku-4.5") }),
request: new Request("https://api.githubcopilot.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "claude-haiku-4.5", messages: [{ role: "user", content: "hello" }] }),
}),
})
expect(yield* Effect.promise(() => event.request.clone().json())).toEqual({
model: "claude-haiku-4.5",
messages: [{ role: "user", content: "hello" }],
})
}),
)
it.effect("classifies compaction requests", () =>
Effect.gen(function* () {
yield* addPlugin()