diff --git a/packages/core/src/session/prompt-cache-diagnostics.ts b/packages/core/src/session/prompt-cache-diagnostics.ts deleted file mode 100644 index 1b38b1da739..00000000000 --- a/packages/core/src/session/prompt-cache-diagnostics.ts +++ /dev/null @@ -1,95 +0,0 @@ -export * as PromptCacheDiagnostics from "./prompt-cache-diagnostics.js" - -import type { LLMRequest } from "@opencode-ai/ai" -import { Hash } from "@opencode-ai/util/hash" - -interface Entry { - readonly label: string - readonly hash: string -} - -export interface Snapshot { - readonly settings: string - readonly tools: ReadonlyArray - readonly system: ReadonlyArray - readonly messages: ReadonlyArray -} - -export type Comparison = - | { readonly status: "initial" } - | { readonly status: "stable"; readonly messages: number } - | { readonly status: "append-only"; readonly previousMessages: number; readonly currentMessages: number } - | { - readonly status: "changed" - readonly component: "settings" | "tools" | "system" | "messages" - readonly index: number - readonly label: string - } - -const hash = (value: unknown) => Hash.sha256(JSON.stringify(value)).slice(0, 16) - -export function snapshot(request: LLMRequest): Snapshot { - return { - settings: hash({ - route: request.model.route.id, - provider: request.model.provider, - model: request.model.id, - modelDefaults: request.model.defaults, - compatibility: request.model.compatibility, - routeDefaults: { - generation: request.model.route.defaults.generation, - providerOptions: request.model.route.defaults.providerOptions, - http: request.model.route.defaults.http, - }, - generation: request.generation, - providerOptions: request.providerOptions, - http: request.http, - toolChoice: request.toolChoice, - cache: request.cache, - }), - tools: request.tools.map((tool) => ({ label: tool.name, hash: hash(tool) })), - system: request.system.map((part, index) => ({ label: `system[${index}]`, hash: hash(part) })), - messages: request.messages.map((message, index) => ({ - label: message.id ?? `${message.role}[${index}]`, - hash: hash(message), - })), - } -} - -export function compare(previous: Snapshot | undefined, current: Snapshot): Comparison { - if (!previous) return { status: "initial" } - if (previous.settings !== current.settings) - return { - status: "changed", - component: "settings", - index: 0, - label: "model settings", - } - const tools = firstChange(previous.tools, current.tools, false) - if (tools) return { status: "changed", component: "tools", ...tools } - const system = firstChange(previous.system, current.system, false) - if (system) return { status: "changed", component: "system", ...system } - const messages = firstChange(previous.messages, current.messages, true) - if (messages) return { status: "changed", component: "messages", ...messages } - if (previous.messages.length === current.messages.length) - return { status: "stable", messages: current.messages.length } - return { - status: "append-only", - previousMessages: previous.messages.length, - currentMessages: current.messages.length, - } -} - -function firstChange(previous: ReadonlyArray, current: ReadonlyArray, allowAppend: boolean) { - const index = previous.findIndex((entry, index) => entry.hash !== current[index]?.hash) - if (index >= 0) - return { - index, - label: current[index]?.label ?? previous[index]?.label ?? `entry[${index}]`, - } - if (current.length === previous.length || (allowAppend && current.length > previous.length)) return - return { - index: previous.length, - label: current[previous.length]?.label ?? `entry[${previous.length}]`, - } -} diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 15550fc6eef..9553e912a2c 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -1,7 +1,7 @@ export * as SessionRunnerLLM from "./llm.js" import { Message } from "@opencode-ai/ai" -import { Cause, Config, Effect, Exit, FiberMap, Layer, Pull, Schedule } from "effect" +import { Cause, Effect, Exit, FiberMap, Layer, Pull, Schedule } from "effect" import { Database } from "../../database/database.js" import { Bus } from "../../bus.js" import { InstructionState } from "../instruction-state.js" @@ -24,7 +24,6 @@ import { SessionRunnerRetry } from "./retry.js" import { SessionStep } from "./step.js" import { ToolOutput } from "../../tool-output.js" import { PluginSupervisor } from "../../plugin/supervisor.js" -import { PromptCacheDiagnostics } from "../prompt-cache-diagnostics.js" import { MAX_STEPS_PROMPT } from "./max-steps.js" const CONTINUE_AFTER_INCOMPLETE_STREAM = @@ -42,32 +41,6 @@ const layer = Layer.effect( const plugins = yield* PluginSupervisor.Service const title = yield* SessionTitle.Service const steps = yield* SessionStep.make - const diagnostics = yield* Config.boolean("OPENCODE_PROMPT_CACHE_DIAGNOSTICS").pipe( - Config.withDefault(false), - Effect.orDie, - ) - const promptCacheSnapshots = diagnostics ? new Map() : undefined - const diagnosePromptCache = Effect.fn("SessionRunner.diagnosePromptCache")(function* ( - sessionID: SessionSchema.ID, - request: Parameters[0], - ) { - if (!promptCacheSnapshots) return - const current = PromptCacheDiagnostics.snapshot(request) - const comparison = PromptCacheDiagnostics.compare(promptCacheSnapshots.get(sessionID), current) - promptCacheSnapshots.delete(sessionID) - promptCacheSnapshots.set(sessionID, current) - const oldest = promptCacheSnapshots.keys().next().value - if (promptCacheSnapshots.size > 100 && oldest !== undefined) promptCacheSnapshots.delete(oldest) - yield* Effect.logInfo("prompt cache prefix").pipe( - Effect.annotateLogs({ - sessionID, - toolCount: current.tools.length, - systemParts: current.system.length, - messageCount: current.messages.length, - ...comparison, - }), - ) - }) // Title generation starts once input is visible and must not delay model execution. const titles = yield* FiberMap.make() @@ -243,7 +216,6 @@ const layer = Layer.effect( toolChoice: stepLimitReached ? "none" : undefined, webSocket: "session", }) - yield* diagnosePromptCache(sessionID, prepared.request) const outcome = yield* steps.attempt({ sessionID, assistantMessageID, diff --git a/packages/core/test/prompt-cache-diagnostics.test.ts b/packages/core/test/prompt-cache-diagnostics.test.ts deleted file mode 100644 index 092f23b7da4..00000000000 --- a/packages/core/test/prompt-cache-diagnostics.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { GenerationOptions, LLM, LLMRequest, Message, LanguageModel, ToolDefinition } from "@opencode-ai/ai" -import { OpenAIChat } from "@opencode-ai/ai/protocols" -import { PromptCacheDiagnostics } from "@opencode-ai/core/session/prompt-cache-diagnostics" - -const model = LanguageModel.make({ id: "test", provider: "test", route: OpenAIChat.route }) -const tool = ToolDefinition.make({ - name: "read", - description: "Read a file", - inputSchema: { type: "object", properties: {} }, -}) - -const request = LLM.request({ - model, - system: "System", - prompt: "First", - tools: [tool], -}) -const compare = (current: LLMRequest) => - PromptCacheDiagnostics.compare(PromptCacheDiagnostics.snapshot(request), PromptCacheDiagnostics.snapshot(current)) - -describe("PromptCacheDiagnostics", () => { - test("distinguishes initial and stable requests", () => { - const snapshot = PromptCacheDiagnostics.snapshot(request) - expect(PromptCacheDiagnostics.compare(undefined, snapshot)).toEqual({ status: "initial" }) - expect(PromptCacheDiagnostics.compare(snapshot, snapshot)).toEqual({ status: "stable", messages: 1 }) - }) - - test("recognizes append-only history", () => { - const current = LLMRequest.update(request, { messages: [...request.messages, Message.assistant("Second")] }) - expect(compare(current)).toEqual({ status: "append-only", previousMessages: 1, currentMessages: 2 }) - }) - - test("detects cache-sensitive setting changes", () => { - const current = LLMRequest.update(request, { generation: GenerationOptions.make({ temperature: 0.5 }) }) - expect(compare(current)).toEqual({ status: "changed", component: "settings", index: 0, label: "model settings" }) - }) - - test("finds the first changed prefix component", () => { - const changedTool = ToolDefinition.make({ ...tool, description: "Read one file" }) - const current = LLMRequest.update(request, { tools: [changedTool] }) - expect(compare(current)).toEqual({ status: "changed", component: "tools", index: 0, label: "read" }) - }) - - test("treats appended tools as a prefix change", () => { - const write = ToolDefinition.make({ - name: "write", - description: "Write a file", - inputSchema: { type: "object", properties: {} }, - }) - const current = LLMRequest.update(request, { tools: [...request.tools, write] }) - expect(compare(current)).toEqual({ status: "changed", component: "tools", index: 1, label: "write" }) - }) -}) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 83cc4d98714..9f0ffba6309 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -45,7 +45,6 @@ import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator import { SessionRunner } from "@opencode-ai/core/session/runner/index" import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" -import { PromptCacheDiagnostics } from "@opencode-ai/core/session/prompt-cache-diagnostics" import { SessionUsage } from "@opencode-ai/core/session/usage" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" @@ -1642,12 +1641,19 @@ describe("SessionRunnerLLM", () => { s.systemBaseline = "Changed context" yield* s.runPrompt("Second") - expect( - PromptCacheDiagnostics.compare( - PromptCacheDiagnostics.snapshot(s.requests[0]), - PromptCacheDiagnostics.snapshot(s.requests[1]), - ), - ).toEqual({ status: "append-only", previousMessages: 1, currentMessages: 3 }) + for (const field of [ + "model", + "generation", + "providerOptions", + "http", + "toolChoice", + "cache", + "tools", + "system", + ] as const) + expect(s.requests[1][field]).toEqual(s.requests[0][field]) + expect(s.requests[0].messages).toHaveLength(1) + expect(s.requests[1].messages.slice(0, 1)).toEqual([...s.requests[0].messages]) expect(s.requests.map((request) => request.system.map((part) => part.text))).toEqual([ [defaultSystem, "Initial context"], [defaultSystem, "Initial context"],