fix(core): restore default model headers (#36975)
This commit is contained in:
@@ -9,6 +9,7 @@ import { makeLocationNode } from "../effect/app-node"
|
|||||||
import { llmClient } from "../effect/app-node-platform"
|
import { llmClient } from "../effect/app-node-platform"
|
||||||
import { SessionEvent } from "./event"
|
import { SessionEvent } from "./event"
|
||||||
import type { SessionMessage } from "./message"
|
import type { SessionMessage } from "./message"
|
||||||
|
import { SessionModelHeaders } from "./model-headers"
|
||||||
import { SessionRunnerModel } from "./runner/model"
|
import { SessionRunnerModel } from "./runner/model"
|
||||||
import { SessionSchema } from "./schema"
|
import { SessionSchema } from "./schema"
|
||||||
import { toSessionError } from "./to-session-error"
|
import { toSessionError } from "./to-session-error"
|
||||||
@@ -66,7 +67,7 @@ type Dependencies = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AutoInput = {
|
export type AutoInput = {
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly session: SessionSchema.Info
|
||||||
readonly messages: readonly SessionMessage.Info[]
|
readonly messages: readonly SessionMessage.Info[]
|
||||||
readonly model: Model
|
readonly model: Model
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ export type ManualInput = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Plan = {
|
type Plan = {
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly session: SessionSchema.Info
|
||||||
readonly model: Model
|
readonly model: Model
|
||||||
readonly reason: SessionMessage.Compaction["reason"]
|
readonly reason: SessionMessage.Compaction["reason"]
|
||||||
readonly prompt: string
|
readonly prompt: string
|
||||||
@@ -230,7 +231,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
})
|
})
|
||||||
const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) {
|
const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) {
|
||||||
yield* dependencies.events.publish(SessionEvent.Compaction.Started, {
|
yield* dependencies.events.publish(SessionEvent.Compaction.Started, {
|
||||||
sessionID: plan.sessionID,
|
sessionID: plan.session.id,
|
||||||
reason: plan.reason,
|
reason: plan.reason,
|
||||||
recent: plan.recent,
|
recent: plan.recent,
|
||||||
inputID: plan.inputID,
|
inputID: plan.inputID,
|
||||||
@@ -242,6 +243,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
.stream(
|
.stream(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: plan.model,
|
model: plan.model,
|
||||||
|
http: { headers: SessionModelHeaders.make(plan.session) },
|
||||||
messages: [Message.user(plan.prompt)],
|
messages: [Message.user(plan.prompt)],
|
||||||
tools: [],
|
tools: [],
|
||||||
}),
|
}),
|
||||||
@@ -256,7 +258,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
if (LLMEvent.is.textDelta(event)) {
|
if (LLMEvent.is.textDelta(event)) {
|
||||||
chunks.push(event.text)
|
chunks.push(event.text)
|
||||||
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
|
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
|
||||||
sessionID: plan.sessionID,
|
sessionID: plan.session.id,
|
||||||
text: event.text,
|
text: event.text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -270,7 +272,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
Effect.onInterrupt(() =>
|
Effect.onInterrupt(() =>
|
||||||
plan.reason === "auto"
|
plan.reason === "auto"
|
||||||
? failed({
|
? failed({
|
||||||
sessionID: plan.sessionID,
|
sessionID: plan.session.id,
|
||||||
reason: plan.reason,
|
reason: plan.reason,
|
||||||
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
|
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
|
||||||
inputID: plan.inputID,
|
inputID: plan.inputID,
|
||||||
@@ -282,14 +284,14 @@ const make = (dependencies: Dependencies) => {
|
|||||||
if (failure || !summary.trim()) {
|
if (failure || !summary.trim()) {
|
||||||
const error = failure ?? { type: "compaction.failed" as const, message: "Compaction produced no summary" }
|
const error = failure ?? { type: "compaction.failed" as const, message: "Compaction produced no summary" }
|
||||||
return yield* failed({
|
return yield* failed({
|
||||||
sessionID: plan.sessionID,
|
sessionID: plan.session.id,
|
||||||
reason: plan.reason,
|
reason: plan.reason,
|
||||||
error,
|
error,
|
||||||
inputID: plan.inputID,
|
inputID: plan.inputID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
||||||
sessionID: plan.sessionID,
|
sessionID: plan.session.id,
|
||||||
reason: plan.reason,
|
reason: plan.reason,
|
||||||
text: summary,
|
text: summary,
|
||||||
recent: plan.recent,
|
recent: plan.recent,
|
||||||
@@ -300,14 +302,14 @@ const make = (dependencies: Dependencies) => {
|
|||||||
const content = planContent(input.messages, config.tokens)
|
const content = planContent(input.messages, config.tokens)
|
||||||
if (content)
|
if (content)
|
||||||
return yield* execute({
|
return yield* execute({
|
||||||
sessionID: input.sessionID,
|
session: input.session,
|
||||||
model: input.model,
|
model: input.model,
|
||||||
reason: "auto",
|
reason: "auto",
|
||||||
...content,
|
...content,
|
||||||
})
|
})
|
||||||
const error = { type: "compaction.unavailable" as const, message: "Nothing to compact yet" }
|
const error = { type: "compaction.unavailable" as const, message: "Nothing to compact yet" }
|
||||||
return yield* failed({
|
return yield* failed({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.session.id,
|
||||||
reason: "auto",
|
reason: "auto",
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
@@ -348,7 +350,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
)
|
)
|
||||||
if ("status" in resolved) return resolved
|
if ("status" in resolved) return resolved
|
||||||
return yield* execute({
|
return yield* execute({
|
||||||
sessionID: input.session.id,
|
session: input.session,
|
||||||
model: resolved.model,
|
model: resolved.model,
|
||||||
reason: "manual",
|
reason: "manual",
|
||||||
inputID: input.inputID,
|
inputID: input.inputID,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
@@ -51,7 +51,7 @@ import { AgentNotFoundError, StepFailedError } from "../error"
|
|||||||
import { toSessionError } from "../to-session-error"
|
import { toSessionError } from "../to-session-error"
|
||||||
import { SessionRunnerRetry } from "./retry"
|
import { SessionRunnerRetry } from "./retry"
|
||||||
import { PluginSupervisor } from "../../plugin/supervisor"
|
import { PluginSupervisor } from "../../plugin/supervisor"
|
||||||
import { Flag } from "../../flag/flag"
|
import { SessionModelHeaders } from "../model-headers"
|
||||||
|
|
||||||
type StepTokens = {
|
type StepTokens = {
|
||||||
readonly input: number
|
readonly input: number
|
||||||
@@ -187,7 +187,7 @@ const layer = Layer.effect(
|
|||||||
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
|
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
|
||||||
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
|
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
|
||||||
const context = history.entries.map((entry) => entry.message)
|
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))) {
|
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
|
||||||
const compacted = yield* compaction.compact(compactionInput)
|
const compacted = yield* compaction.compact(compactionInput)
|
||||||
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
||||||
@@ -199,11 +199,7 @@ const layer = Layer.effect(
|
|||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
model,
|
model,
|
||||||
http: {
|
http: {
|
||||||
headers: {
|
headers: SessionModelHeaders.make(session),
|
||||||
"x-opencode-project": session.projectID,
|
|
||||||
"x-opencode-session": session.id,
|
|
||||||
"x-opencode-client": Flag.OPENCODE_CLIENT,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
providerOptions: { openai: { promptCacheKey } },
|
providerOptions: { openai: { promptCacheKey } },
|
||||||
system: [agentInfo.system ? agentInfo.system : SessionRunnerSystemPrompt.provider(model), history.initial]
|
system: [agentInfo.system ? agentInfo.system : SessionRunnerSystemPrompt.provider(model), history.initial]
|
||||||
@@ -341,7 +337,7 @@ const layer = Layer.effect(
|
|||||||
recoverOverflow &&
|
recoverOverflow &&
|
||||||
!publisher.hasRetryEvidence() &&
|
!publisher.hasRetryEvidence() &&
|
||||||
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
|
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
|
||||||
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model }))).status ===
|
(yield* restore(recoverOverflow({ session, messages: context, model }))).status ===
|
||||||
"completed"
|
"completed"
|
||||||
)
|
)
|
||||||
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
|
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { makeLocationNode } from "../effect/app-node"
|
|||||||
import { llmClient } from "../effect/app-node-platform"
|
import { llmClient } from "../effect/app-node-platform"
|
||||||
import { SessionEvent } from "./event"
|
import { SessionEvent } from "./event"
|
||||||
import { SessionHistory } from "./history"
|
import { SessionHistory } from "./history"
|
||||||
|
import { SessionModelHeaders } from "./model-headers"
|
||||||
import { SessionRunnerModel } from "./runner/model"
|
import { SessionRunnerModel } from "./runner/model"
|
||||||
import { SessionSchema } from "./schema"
|
import { SessionSchema } from "./schema"
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ const make = (dependencies: Dependencies) => {
|
|||||||
.stream(
|
.stream(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: resolved.model,
|
model: resolved.model,
|
||||||
|
http: { headers: SessionModelHeaders.make(session) },
|
||||||
system: agent.system,
|
system: agent.system,
|
||||||
messages: [Message.user(firstUser.text)],
|
messages: [Message.user(firstUser.text)],
|
||||||
tools: [],
|
tools: [],
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { SessionStore } from "@opencode-ai/core/session/store"
|
|||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
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 { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
|
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
|
||||||
import { asc, eq } from "drizzle-orm"
|
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 events = yield* EventV2.Service
|
||||||
const store = yield* SessionStore.Service
|
const store = yield* SessionStore.Service
|
||||||
const sessionID = SessionV2.ID.make("ses_manual_compaction")
|
const sessionID = SessionV2.ID.make("ses_manual_compaction")
|
||||||
|
const parentID = SessionV2.ID.make("ses_manual_compaction_parent")
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
id: SessionMessage.ID.create(),
|
id: SessionMessage.ID.create(),
|
||||||
type: "user" as const,
|
type: "user" as const,
|
||||||
@@ -121,6 +124,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
|||||||
.values({
|
.values({
|
||||||
id: sessionID,
|
id: sessionID,
|
||||||
project_id: Project.ID.global,
|
project_id: Project.ID.global,
|
||||||
|
parent_id: parentID,
|
||||||
slug: "manual-compaction",
|
slug: "manual-compaction",
|
||||||
directory: "/project",
|
directory: "/project",
|
||||||
title: "Manual compaction",
|
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(Array.from(yield* Fiber.join(delta)).map((event) => event.data.text)).toEqual(["manual summary"])
|
||||||
|
|
||||||
expect(requests).toHaveLength(1)
|
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(requests[0]?.generation).toBeUndefined()
|
||||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
|
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
|
||||||
expect(yield* store.context(sessionID)).toMatchObject([
|
expect(yield* store.context(sessionID)).toMatchObject([
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
|||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
@@ -3086,6 +3087,9 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* session.resume(sessionID)
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
expect(requests[0]?.http?.headers).toEqual({
|
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-project": Project.ID.global,
|
||||||
"x-opencode-session": sessionID,
|
"x-opencode-session": sessionID,
|
||||||
"x-opencode-client": Flag.OPENCODE_CLIENT,
|
"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", () =>
|
it.effect("runs different sessions concurrently", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { SessionTitle } from "@opencode-ai/core/session/title"
|
|||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
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 { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { DateTime, Effect, Layer, Stream } from "effect"
|
import { DateTime, Effect, Layer, Stream } from "effect"
|
||||||
import { testEffect } from "./lib/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)
|
yield* title.generateForFirstPrompt(session)
|
||||||
|
|
||||||
expect(requests).toHaveLength(1)
|
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")
|
expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build")
|
||||||
const renamed = yield* store.get(sessionID)
|
const renamed = yield* store.get(sessionID)
|
||||||
expect(renamed?.title).toBe("Generated Title")
|
expect(renamed?.title).toBe("Generated Title")
|
||||||
|
|||||||
Reference in New Issue
Block a user