Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/opencode-session-header-followup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Fix "missing x-opencode-session" errors from memory consolidation, prompt enhancement, and the roll-call diagnostic command when using an OpenCode-managed model.
2 changes: 2 additions & 0 deletions packages/kilo-memory/src/effect/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export namespace MemoryCapture {
try: () =>
input.model.run({
handle: model!,
sessionID: input.sessionID,
system: digestPrompt,
prompt: body,
timeoutMs: state.capture.timeoutMs,
Expand Down Expand Up @@ -360,6 +361,7 @@ export namespace MemoryCapture {
try: () =>
input.model.run({
handle: model!,
sessionID: input.sessionID,
system: typedPrompt,
prompt: body,
timeoutMs: state.capture.timeoutMs,
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-memory/src/effect/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export namespace MemoryPorts {
readonly resolve: (input: { configured?: string; session: ModelRef }) => Effect.Effect<ModelResolution, MemoryError>
readonly run: (input: {
handle: ModelHandle
sessionID: string
system: string
prompt: string
timeoutMs: number
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/kilocode/cli/cmd/roll-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ function loadDeps() {
import("../../../provider/provider"),
import("../../../effect/runtime-flags"),
import("ai"),
]).then(([runtime, provider, flags, ai]) => ({
import("../../provider/opencode-session-headers"),
]).then(([runtime, provider, flags, ai, headers]) => ({
AppRuntime: runtime.AppRuntime,
Provider: provider.Provider,
RuntimeFlags: flags.RuntimeFlags,
generateText: ai.generateText,
opencodeSessionHeaders: headers.opencodeSessionHeaders,
}))
}
function deps() {
Expand Down Expand Up @@ -310,7 +312,7 @@ async function call(
start: number,
): Promise<Omit<Result, "model">> {
try {
const { AppRuntime, RuntimeFlags, generateText } = await deps()
const { AppRuntime, RuntimeFlags, generateText, opencodeSessionHeaders } = await deps()
const language = await lang(model)
const sessionID = randomUUID()
const options = ProviderTransform.options({ model, sessionID })
Expand All @@ -332,6 +334,7 @@ async function call(
topP,
topK,
providerOptions,
headers: opencodeSessionHeaders({ providerID: model.providerID, sessionID }),
})

return {
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/kilocode/enhance-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { generateText } from "ai"
import { randomUUID } from "crypto"
import { mergeDeep } from "remeda"
import { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { AppRuntime } from "@/effect/app-runtime"
import { Effect } from "effect"
import * as Log from "@opencode-ai/core/util/log"
import { opencodeSessionHeaders } from "@/kilocode/provider/opencode-session-headers"

const log = Log.create({ service: "enhance-prompt" })

Expand Down Expand Up @@ -50,6 +52,9 @@ export async function enhancePrompt(text: string): Promise<string> {
),
maxRetries: 3,
system: INSTRUCTION,
// Each call is a standalone rewrite, not part of a multi-turn conversation; a fresh ID
// per call still satisfies the opencode API's "stable per-conversation ID" requirement.
headers: opencodeSessionHeaders({ providerID: resolved.model.providerID, sessionID: randomUUID() }),
messages: [{ role: "user" as const, content: `Draft prompt to enhance, not answer:\n\n${text}` }],
})

Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/src/kilocode/memory/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Snapshot } from "@/snapshot"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { SessionID } from "@/session/schema"
import { opencodeSessionHeaders } from "@/kilocode/provider/opencode-session-headers"

const log = Log.create({ service: "memory.ports" })

Expand Down Expand Up @@ -174,6 +175,7 @@ async function memoryText(input: {
system: string
prompt: string
timeoutMs: number
sessionID: string
temperature?: number
topP?: number
topK?: number
Expand All @@ -193,6 +195,7 @@ async function memoryText(input: {
topP: input.topP,
topK: input.topK,
maxRetries: 1,
headers: opencodeSessionHeaders({ providerID: input.source.providerID, sessionID: input.sessionID }),
}
const work = async () => {
if (!openai) return generateText(common)
Expand Down Expand Up @@ -314,7 +317,7 @@ export namespace MemoryModel {
const language = yield* input.provider.getLanguage(source)
return { handle: modelOptions(source, language), ...(reason ? { fallback: { reason } } : {}) }
}).pipe(Effect.mapError(MemoryError.from)),
run: ({ handle, system, prompt, timeoutMs, signal }) => {
run: ({ handle, sessionID, system, prompt, timeoutMs, signal }) => {
const resolved = handle as ModelHandle
return memoryText({
source: resolved.source,
Expand All @@ -323,6 +326,7 @@ export namespace MemoryModel {
system,
prompt,
timeoutMs,
sessionID,
temperature: resolved.temperature,
topP: resolved.topP,
topK: resolved.topK,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { USER_AGENT } from "@/installation"

/**
* The OpenCode Go managed-inference API requires a stable `x-opencode-session` header on
* every request routed through an `opencode*` provider (anaconda/kilocode#13723). The main
* chat path gets this from `LLMRequestPrep.prepare` (session/llm/request.ts); any other
* caller that resolves a language model and invokes the `ai` SDK directly must attach it here.
*/
export function opencodeSessionHeaders(input: { providerID: string; sessionID: string }): Record<string, string> {
return input.providerID.startsWith("opencode")
? {
"x-opencode-session": input.sessionID,
"User-Agent": USER_AGENT,
}
: {}
}
46 changes: 42 additions & 4 deletions packages/opencode/test/kilocode/memory/memory-ports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,48 @@ describe("memory ports", () => {
expect(seen).toEqual(["memory-config-model", "fake-memory-model"])
})

test("model port sends x-opencode-session for opencode-managed memory models", async () => {
const calls: unknown[] = []
const port = MemoryModel.port({
provider: provider({ providerID: ProviderV2.ID.make("opencode"), calls }),
})
const resolved = await Effect.runPromise(port.resolve({ session: ref }))

await port.run({
handle: resolved.handle,
sessionID: "ses_memory_headers",
system: "system",
prompt: "prompt",
timeoutMs: 30_000,
})

const opts = calls[0] as { headers?: Record<string, string> }
expect(opts.headers?.["x-opencode-session"]).toBe("ses_memory_headers")
})

test("model port omits opencode headers for non-opencode memory models", async () => {
const calls: unknown[] = []
const port = MemoryModel.port({ provider: provider({ calls }) })
const resolved = await Effect.runPromise(port.resolve({ session: ref }))

await port.run({
handle: resolved.handle,
sessionID: "ses_memory_headers",
system: "system",
prompt: "prompt",
timeoutMs: 30_000,
})

const opts = calls[0] as { headers?: Record<string, string> }
expect(opts.headers?.["x-opencode-session"]).toBeUndefined()
})

test("model port asks OpenAI-compatible providers for a non-streaming JSON response", async () => {
const calls: unknown[] = []
const port = MemoryModel.port({ provider: provider({ npm: "@ai-sdk/openai-compatible", calls }) })
const resolved = await Effect.runPromise(port.resolve({ session: ref }))

await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 })
await port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000 })

const opts = calls[0] as { providerOptions?: Record<string, { stream?: boolean }> }
expect(opts.providerOptions?.test?.stream).toBe(false)
Expand All @@ -321,7 +357,7 @@ describe("memory ports", () => {
})
const resolved = await Effect.runPromise(port.resolve({ session: ref }))

await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 })
await port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000 })

const opts = calls[0] as { providerOptions?: Record<string, { stream?: boolean }> }
expect(opts.providerOptions?.openai?.stream).toBe(false)
Expand Down Expand Up @@ -374,6 +410,7 @@ describe("memory ports", () => {
const resolved = await Effect.runPromise(port.resolve({ session: ref }))
const result = await port.run({
handle: resolved.handle,
sessionID: "ses_test",
system: "system",
prompt: "prompt",
timeoutMs: 30_000,
Expand All @@ -400,7 +437,7 @@ describe("memory ports", () => {
const port = MemoryModel.port({ provider: provider({ outputs: [err, "{}"], calls }) })
const resolved = await Effect.runPromise(port.resolve({ session: ref }))

await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 })
await port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000 })

expect(calls).toHaveLength(2)
const opts = calls[0] as { providerOptions?: Record<string, { stream?: boolean }> }
Expand All @@ -412,7 +449,7 @@ describe("memory ports", () => {
const resolved = await Effect.runPromise(port.resolve({ session: ref }))

await expect(
port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 1 }),
port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 1 }),
).rejects.toMatchObject({ name: "TimeoutError", message: "memory model timed out" })
})

Expand Down Expand Up @@ -442,6 +479,7 @@ describe("memory ports", () => {

await port.run({
handle: resolved.handle,
sessionID: "ses_test",
system: "system",
prompt: "prompt",
timeoutMs: 30_000,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, test } from "bun:test"
import { opencodeSessionHeaders } from "@/kilocode/provider/opencode-session-headers"

describe("opencodeSessionHeaders", () => {
test("attaches x-opencode-session for opencode-managed providers", () => {
const headers = opencodeSessionHeaders({ providerID: "opencode", sessionID: "ses_abc" })
expect(headers["x-opencode-session"]).toBe("ses_abc")
})

test("matches any opencode-prefixed provider id (e.g. opencode-go)", () => {
const headers = opencodeSessionHeaders({ providerID: "opencode-go", sessionID: "ses_abc" })
expect(headers["x-opencode-session"]).toBe("ses_abc")
})

test("returns no headers for non-opencode providers", () => {
const headers = opencodeSessionHeaders({ providerID: "anthropic", sessionID: "ses_abc" })
expect(headers).toEqual({})
})

test("returns no headers for providers that merely contain the word opencode", () => {
const headers = opencodeSessionHeaders({ providerID: "my-opencode-proxy", sessionID: "ses_abc" })
expect(headers).toEqual({})
})
})
Loading