mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
feat(core): use copilot utility models for titles
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user