fix(core): restore default model headers (#36975)

This commit is contained in:
Aiden Cline 2026-07-14 20:50:32 -05:00 committed by GitHub
parent ba1f3d3d32
commit 72cf7f12f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 79 additions and 18 deletions

View file

@ -9,6 +9,7 @@ import { makeLocationNode } from "../effect/app-node"
import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import type { SessionMessage } from "./message"
import { SessionModelHeaders } from "./model-headers"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
import { toSessionError } from "./to-session-error"
@ -66,7 +67,7 @@ type Dependencies = {
}
export type AutoInput = {
readonly sessionID: SessionSchema.ID
readonly session: SessionSchema.Info
readonly messages: readonly SessionMessage.Info[]
readonly model: Model
}
@ -78,7 +79,7 @@ export type ManualInput = {
}
type Plan = {
readonly sessionID: SessionSchema.ID
readonly session: SessionSchema.Info
readonly model: Model
readonly reason: SessionMessage.Compaction["reason"]
readonly prompt: string
@ -230,7 +231,7 @@ const make = (dependencies: Dependencies) => {
})
const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) {
yield* dependencies.events.publish(SessionEvent.Compaction.Started, {
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
recent: plan.recent,
inputID: plan.inputID,
@ -242,6 +243,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: plan.model,
http: { headers: SessionModelHeaders.make(plan.session) },
messages: [Message.user(plan.prompt)],
tools: [],
}),
@ -256,7 +258,7 @@ const make = (dependencies: Dependencies) => {
if (LLMEvent.is.textDelta(event)) {
chunks.push(event.text)
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
sessionID: plan.sessionID,
sessionID: plan.session.id,
text: event.text,
})
}
@ -270,7 +272,7 @@ const make = (dependencies: Dependencies) => {
Effect.onInterrupt(() =>
plan.reason === "auto"
? failed({
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
inputID: plan.inputID,
@ -282,14 +284,14 @@ const make = (dependencies: Dependencies) => {
if (failure || !summary.trim()) {
const error = failure ?? { type: "compaction.failed" as const, message: "Compaction produced no summary" }
return yield* failed({
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
error,
inputID: plan.inputID,
})
}
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
text: summary,
recent: plan.recent,
@ -300,14 +302,14 @@ const make = (dependencies: Dependencies) => {
const content = planContent(input.messages, config.tokens)
if (content)
return yield* execute({
sessionID: input.sessionID,
session: input.session,
model: input.model,
reason: "auto",
...content,
})
const error = { type: "compaction.unavailable" as const, message: "Nothing to compact yet" }
return yield* failed({
sessionID: input.sessionID,
sessionID: input.session.id,
reason: "auto",
error,
})
@ -348,7 +350,7 @@ const make = (dependencies: Dependencies) => {
)
if ("status" in resolved) return resolved
return yield* execute({
sessionID: input.session.id,
session: input.session,
model: resolved.model,
reason: "manual",
inputID: input.inputID,

View file

@ -0,0 +1,15 @@
export * as SessionModelHeaders from "./model-headers"
import { Flag } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { SessionSchema } from "./schema"
export const make = (session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">) => ({
"x-session-affinity": session.id,
"X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": session.projectID,
"x-opencode-session": session.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
})

View file

@ -51,7 +51,7 @@ import { AgentNotFoundError, StepFailedError } from "../error"
import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry"
import { PluginSupervisor } from "../../plugin/supervisor"
import { Flag } from "../../flag/flag"
import { SessionModelHeaders } from "../model-headers"
type StepTokens = {
readonly input: number
@ -187,7 +187,7 @@ const layer = Layer.effect(
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
const context = history.entries.map((entry) => entry.message)
const compactionInput = { sessionID: session.id, messages: context, model }
const compactionInput = { session, messages: context, model }
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
const compacted = yield* compaction.compact(compactionInput)
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
@ -199,11 +199,7 @@ const layer = Layer.effect(
const request = LLM.request({
model,
http: {
headers: {
"x-opencode-project": session.projectID,
"x-opencode-session": session.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
},
headers: SessionModelHeaders.make(session),
},
providerOptions: { openai: { promptCacheKey } },
system: [agentInfo.system ? agentInfo.system : SessionRunnerSystemPrompt.provider(model), history.initial]
@ -341,7 +337,7 @@ const layer = Layer.effect(
recoverOverflow &&
!publisher.hasRetryEvidence() &&
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model }))).status ===
(yield* restore(recoverOverflow({ session, messages: context, model }))).status ===
"completed"
)
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const

View file

@ -9,6 +9,7 @@ import { makeLocationNode } from "../effect/app-node"
import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
@ -54,6 +55,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: resolved.model,
http: { headers: SessionModelHeaders.make(session) },
system: agent.system,
messages: [Message.user(firstUser.text)],
tools: [],

View file

@ -18,6 +18,8 @@ import { SessionStore } from "@opencode-ai/core/session/store"
import { SessionV2 } from "@opencode-ai/core/session"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
import { asc, eq } from "drizzle-orm"
@ -104,6 +106,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
const events = yield* EventV2.Service
const store = yield* SessionStore.Service
const sessionID = SessionV2.ID.make("ses_manual_compaction")
const parentID = SessionV2.ID.make("ses_manual_compaction_parent")
const userMessage = {
id: SessionMessage.ID.create(),
type: "user" as const,
@ -121,6 +124,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
.values({
id: sessionID,
project_id: Project.ID.global,
parent_id: parentID,
slug: "manual-compaction",
directory: "/project",
title: "Manual compaction",
@ -151,6 +155,15 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
expect(Array.from(yield* Fiber.join(delta)).map((event) => event.data.text)).toEqual(["manual summary"])
expect(requests).toHaveLength(1)
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
"x-parent-session-id": parentID,
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
})
expect(requests[0]?.generation).toBeUndefined()
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
expect(yield* store.context(sessionID)).toMatchObject([

View file

@ -20,6 +20,7 @@ import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { EventTable } from "@opencode-ai/core/event/sql"
import { Project } from "@opencode-ai/core/project"
@ -3086,6 +3087,9 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
@ -3093,6 +3097,25 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("adds the parent session header to child model requests", () =>
Effect.gen(function* () {
const session = yield* setup
const parentID = SessionV2.ID.make("ses_runner_parent")
const { db } = yield* Database.Service
yield* db
.update(SessionTable)
.set({ parent_id: parentID })
.where(eq(SessionTable.id, sessionID))
.run()
.pipe(Effect.orDie)
yield* admit(session, "Run child request")
yield* session.resume(sessionID)
expect(requests[0]?.http?.headers?.["x-parent-session-id"]).toBe(parentID)
}),
)
it.effect("runs different sessions concurrently", () =>
Effect.gen(function* () {
const session = yield* setup

View file

@ -17,6 +17,8 @@ import { SessionTitle } from "@opencode-ai/core/session/title"
import { SessionV2 } from "@opencode-ai/core/session"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { DateTime, Effect, Layer, Stream } from "effect"
import { testEffect } from "./lib/effect"
@ -117,6 +119,14 @@ it.effect("generates a title from the sole user message and renames the session"
yield* title.generateForFirstPrompt(session)
expect(requests).toHaveLength(1)
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
})
expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build")
const renamed = yield* store.get(sessionID)
expect(renamed?.title).toBe("Generated Title")