mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(core): share affinity in provider session headers (#51931)
This commit is contained in:
@@ -11,6 +11,7 @@ import { Model } from "../../model.js"
|
||||
import { Agent } from "../../agent.js"
|
||||
import { define } from "@opencode/plugin/effect/plugin"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { SessionAffinity } from "../../session/affinity.js"
|
||||
import type { PluginInternal } from "../internal.js"
|
||||
|
||||
const clientID = "Ov23li8tweQw6odWQebz"
|
||||
@@ -272,7 +273,7 @@ export const GithubCopilotPlugin = define({
|
||||
.pipe(Effect.orElseSucceed(() => undefined))
|
||||
const interaction = interactionType(evt.kind, session?.parentID !== undefined)
|
||||
evt.headers["X-Interaction-Type"] = interaction
|
||||
evt.headers["X-Interaction-Id"] = evt.sessionID
|
||||
evt.headers["X-Interaction-Id"] = session ? SessionAffinity.get(session) : evt.sessionID
|
||||
if (interaction !== "conversation-agent") evt.headers["x-initiator"] = "agent"
|
||||
}),
|
||||
{ providerID: Provider.ID.githubCopilot },
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Bus } from "../../bus.js"
|
||||
import { Integration } from "../../integration.js"
|
||||
import { OauthCallbackPage } from "../../oauth/page.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { SessionAffinity } from "../../session/affinity.js"
|
||||
import type { PluginInternal } from "../internal.js"
|
||||
|
||||
const clientID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
@@ -299,12 +300,16 @@ export const OpenAIPlugin = define({
|
||||
yield* ctx.session.hook(
|
||||
"model.request",
|
||||
(evt) =>
|
||||
Effect.sync(() => {
|
||||
Effect.gen(function* () {
|
||||
if (!chatgpt) return
|
||||
if (evt.baseURL && URL.canParse(evt.baseURL) && new URL(evt.baseURL).origin === "https://api.openai.com")
|
||||
evt.baseURL = codexBaseURL
|
||||
const session = yield* ctx.session
|
||||
.get({ sessionID: evt.sessionID })
|
||||
.pipe(Effect.orElseSucceed(() => undefined))
|
||||
evt.headers.originator = "opencode"
|
||||
evt.headers["session-id"] = evt.sessionID
|
||||
// ChatGPT routes its prompt cache on this header, so children share the parent's.
|
||||
evt.headers["session-id"] = session ? SessionAffinity.get(session) : evt.sessionID
|
||||
}),
|
||||
{ providerID: Provider.ID.openai },
|
||||
)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export * as SessionAffinity from "./affinity.js"
|
||||
|
||||
import type { SessionSchema } from "./schema.js"
|
||||
|
||||
// TODO: Should the `model.request` hook expose affinity so plugins stop deriving it from `ctx.session.get`?
|
||||
/** The Session ID that groups model requests for provider cache routing: children share the parent's, forks the fork source's. */
|
||||
export const get = (session: Pick<SessionSchema.Info, "id" | "parentID" | "fork">) =>
|
||||
session.parentID ?? session.fork?.sessionID ?? session.id
|
||||
@@ -31,6 +31,7 @@ import { Permission } from "../permission.js"
|
||||
import { PluginHooks } from "../plugin/hooks.js"
|
||||
import { QuestionTool } from "../tool/plugin/question.js"
|
||||
import { Tool } from "../tool.js"
|
||||
import { SessionAffinity } from "./affinity.js"
|
||||
import { SessionModelTransport } from "./model-transport.js"
|
||||
import { SessionProviderContext } from "./provider-context.js"
|
||||
import { SessionRunnerModel } from "./runner/model.js"
|
||||
@@ -270,7 +271,7 @@ export const layer = Layer.effect(
|
||||
const entries = Object.entries(shaped.options)
|
||||
const generation = Object.fromEntries(entries.filter(([k]) => GENERATION_KEYS.has(k))) as GenerationOptionsFields
|
||||
const providerOptions = Object.fromEntries(entries.filter(([k]) => !GENERATION_KEYS.has(k)))
|
||||
const affinity = session.parentID ?? session.fork?.sessionID ?? session.id
|
||||
const affinity = SessionAffinity.get(session)
|
||||
const base = LLM.request({
|
||||
model: model.model,
|
||||
http: {
|
||||
|
||||
@@ -183,10 +183,11 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("classifies child-session steps as subagent interactions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
const event = yield* modelRequest((yield* sessions()).child, "primary")
|
||||
const ids = yield* sessions()
|
||||
const event = yield* modelRequest(ids.child, "primary")
|
||||
expect(event.headers).toEqual({
|
||||
"X-Interaction-Type": "conversation-subagent",
|
||||
"X-Interaction-Id": event.sessionID,
|
||||
"X-Interaction-Id": ids.parent,
|
||||
"x-initiator": "agent",
|
||||
})
|
||||
}),
|
||||
@@ -207,10 +208,11 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("classifies compaction requests by kind rather than agent", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
const event = yield* modelRequest((yield* sessions()).child, "compaction", "build")
|
||||
const ids = yield* sessions()
|
||||
const event = yield* modelRequest(ids.child, "compaction", "build")
|
||||
expect(event.headers).toEqual({
|
||||
"X-Interaction-Type": "conversation-compaction",
|
||||
"X-Interaction-Id": event.sessionID,
|
||||
"X-Interaction-Id": ids.parent,
|
||||
"x-initiator": "agent",
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Money } from "@opencode/schema/money"
|
||||
import { Agent } from "@opencode/schema/agent"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { Session } from "@opencode/core/session"
|
||||
import { OpenAIResponses } from "@opencode/ai/protocols/openai-responses"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, DateTime, Effect } from "effect"
|
||||
@@ -41,10 +41,14 @@ function required<T>(value: T | undefined): T {
|
||||
return value
|
||||
}
|
||||
|
||||
const request = Effect.fn(function* (providerID: Provider.ID, baseURL: string) {
|
||||
const request = Effect.fn(function* (
|
||||
providerID: Provider.ID,
|
||||
baseURL: string,
|
||||
sessionID = Session.ID.make("ses_test"),
|
||||
) {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const event = yield* hooks.trigger("session", "model.request", {
|
||||
sessionID: Session.ID.make("ses_test"),
|
||||
sessionID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID, id: Model.ID.make("gpt-5.5") }),
|
||||
kind: "primary",
|
||||
@@ -156,6 +160,12 @@ describe("OpenAIPlugin", () => {
|
||||
expect(custom.headers).not.toHaveProperty("originator")
|
||||
expect(proxy.baseURL).toBe("https://proxy.example/v1?region=us")
|
||||
expect(proxy.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
|
||||
const sessions = yield* Session.Service
|
||||
const location = yield* Location.Service
|
||||
const parent = yield* sessions.create({ location: { directory: location.directory } })
|
||||
const child = yield* sessions.create({ parentID: parent.id })
|
||||
const childRequest = yield* request(Provider.ID.openai, "https://api.openai.com/v1", child.id)
|
||||
expect(childRequest.headers).toMatchObject({ "session-id": parent.id })
|
||||
const eligible = required(yield* models.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
||||
expect(eligible.package).toBe("@opencode/ai/providers/openai")
|
||||
expect(eligible.headers).toMatchObject({ originator: "opencode", "chatgpt-account-id": "acct_123" })
|
||||
|
||||
Reference in New Issue
Block a user