Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden Cline a214ac39de refactor(ai): unify prompt cache configuration 2026-07-31 17:52:32 -05:00
Aiden Cline f6ea6b1762 refactor(ai): promote prompt cache key 2026-07-31 16:59:24 -05:00
24 changed files with 77 additions and 69 deletions
+4 -5
View File
@@ -33,9 +33,10 @@ const model = OpenAI.configure({
//
// - `generation`: common controls such as max tokens, temperature, topP/topK,
// penalties, seed, and stop sequences.
// - `cache`: provider-neutral prompt caching behavior and cache affinity.
// - `providerOptions`: namespaced provider-native behavior. For example,
// OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking
// config, or OpenRouter routing/reasoning.
// OpenAI store behavior, Anthropic thinking, Gemini thinking config, or
// OpenRouter routing/reasoning.
// - `http`: last-resort serializable overlays for final request body, headers,
// and query params. Prefer typed `providerOptions` when a field is stable.
//
@@ -45,9 +46,7 @@ const request = LLM.request({
system: "You are concise and practical.",
prompt: "Tell me a joke",
generation: { maxTokens: 80, temperature: 0.7 },
providerOptions: {
openai: { promptCacheKey: "tutorial-joke" },
},
cache: { mode: "auto", key: "tutorial-joke" },
})
// 3. `generate` sends the request and collects the event stream into one
+11 -7
View File
@@ -10,16 +10,16 @@
//
// Manual `cache: CacheHint` placements on individual parts are preserved and
// count against the four-breakpoint budget; auto only fills remaining slots.
import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options"
import { CacheHint, type CacheExplicit, type CachePolicy } from "./schema/options"
import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages"
const AUTO: CachePolicyObject = {
const AUTO: Omit<CacheExplicit, "mode" | "key"> = {
tools: true,
system: true,
messages: { tail: 1 },
}
const NONE: CachePolicyObject = {}
const NONE: Omit<CacheExplicit, "mode" | "key"> = {}
const BREAKPOINT_CAP = 4
// Resolution rules:
@@ -29,8 +29,8 @@ const BREAKPOINT_CAP = 4
// - "auto" → tools + first/last system + final message boundary.
// - "none" → no auto placement; manual `CacheHint`s still flow.
// - object form → exactly what the caller asked for.
const resolve = (policy: CachePolicy | undefined): CachePolicyObject => {
if (policy === undefined || policy === "auto") return AUTO
const resolve = (policy: CachePolicy | undefined): Omit<CacheExplicit, "mode" | "key"> => {
if (policy === undefined || (policy !== "none" && policy.mode === "auto")) return AUTO
if (policy === "none") return NONE
return policy
}
@@ -103,7 +103,7 @@ const markMessageAt = (
const markMessages = (
messages: ReadonlyArray<Message>,
strategy: NonNullable<CachePolicyObject["messages"]>,
strategy: NonNullable<CacheExplicit["messages"]>,
hint: CacheHint,
budget: Budget,
): ReadonlyArray<Message> => {
@@ -133,7 +133,11 @@ const countHints = (request: LLMRequest) =>
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")) return request
if (
request.model.route.id === "openrouter" &&
(request.cache === undefined || (request.cache !== "none" && request.cache.mode === "auto"))
)
return request
const policy = resolve(request.cache)
if (!policy.tools && !policy.system && !policy.messages) return request
+2 -1
View File
@@ -3,6 +3,7 @@ import type { Content } from "@opencode-ai/schema/tool"
import { HttpTransport } from "../route/transport"
import { Protocol } from "../route/protocol"
import {
cacheKey,
LLMError,
LLMEvent,
Usage,
@@ -542,7 +543,7 @@ const lowerOptions = (request: LLMRequest) => {
return {
...(options.instructions ? { instructions: options.instructions } : {}),
...(options.store !== undefined ? { store: options.store } : {}),
...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
...(cacheKey(request.cache) ? { prompt_cache_key: cacheKey(request.cache) } : {}),
...(options.include ? { include: options.include } : {}),
...(options.reasoningEffort || options.reasoningSummary
? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
@@ -33,7 +33,6 @@ export const ServiceTierSchema = Schema.Literals(ServiceTiers)
export interface Resolved {
readonly instructions?: string
readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: string
readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable>
@@ -50,7 +49,6 @@ export const resolve = (request: LLMRequest): Resolved => {
return {
instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
store: typeof input?.store === "boolean" ? input.store : undefined,
promptCacheKey: typeof input?.promptCacheKey === "string" ? input.promptCacheKey : undefined,
reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
reasoningSummary:
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
@@ -5,7 +5,6 @@ export interface OpenResponsesOptionsInput {
readonly [key: string]: unknown
readonly instructions?: string
readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable>
@@ -17,7 +17,6 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
const openai = Object.fromEntries(
definedEntries({
store: options?.store,
promptCacheKey: options?.promptCacheKey,
reasoningEffort: options?.reasoningEffort,
reasoningSummary: options?.reasoningSummary,
include: options?.include,
+2 -3
View File
@@ -4,7 +4,7 @@ import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import { ProviderID, type CacheHint, type ModelID, type ProviderOptions } from "../schema"
import { cacheKey, ProviderID, type CacheHint, type ModelID, type ProviderOptions } from "../schema"
import type { ProviderPackage } from "../provider-package"
import * as OpenAICompatibleProfiles from "./openai-compatible-profile"
import * as OpenAIChat from "../protocols/openai-chat"
@@ -55,7 +55,6 @@ export interface OpenRouterOptions {
readonly debug?: Readonly<{ echo_upstream_body?: boolean }>
readonly models?: ReadonlyArray<string>
readonly plugins?: ReadonlyArray<OpenRouterPlugin>
readonly promptCacheKey?: string
readonly provider?: OpenRouterProviderRouting
readonly reasoning?: Readonly<{
enabled?: boolean
@@ -122,6 +121,7 @@ export const protocol = Protocol.make({
...body,
messages,
...bodyOptions(request.providerOptions?.openrouter),
...(cacheKey(request.cache) ? { prompt_cache_key: cacheKey(request.cache) } : {}),
} as OpenRouterBody
}),
),
@@ -161,7 +161,6 @@ const bodyOptions = (input: unknown) => {
...(isRecord(debug) ? { debug } : {}),
...(typeof user === "string" ? { user } : {}),
...(isRecord(reasoning) ? { reasoning } : {}),
...(typeof promptCacheKey === "string" ? { prompt_cache_key: promptCacheKey } : {}),
}
}
+18 -14
View File
@@ -256,18 +256,17 @@ export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
ttlSeconds: Schema.optional(Schema.Number),
}) {}
// Auto-placement policy for prompt caching. The protocol-neutral lowering step
// reads this and injects `CacheHint`s at the configured boundaries; the
// per-protocol body builders then translate those hints into wire markers as
// usual. `"auto"` is the recommended default for agent loops — it places
// breakpoints at the last tool definition, the first and last distinct system
// parts, and the conversation tail. The rolling message breakpoint keeps a
// prior cache entry within Anthropic/Bedrock's 20-block lookback during long
// tool loops.
//
// Pass `"none"` to opt out entirely (the legacy behavior). Pass the granular
// object form to override individual choices.
export const CachePolicyObject = Schema.Struct({
const CacheKey = { key: Schema.optional(Schema.String) }
export const CacheAuto = Schema.Struct({
mode: Schema.Literal("auto"),
...CacheKey,
})
export type CacheAuto = Schema.Schema.Type<typeof CacheAuto>
export const CacheExplicit = Schema.Struct({
mode: Schema.Literal("explicit"),
...CacheKey,
tools: Schema.optional(Schema.Boolean),
system: Schema.optional(Schema.Boolean),
messages: Schema.optional(
@@ -279,7 +278,12 @@ export const CachePolicyObject = Schema.Struct({
),
ttlSeconds: Schema.optional(Schema.Number),
})
export type CachePolicyObject = Schema.Schema.Type<typeof CachePolicyObject>
export type CacheExplicit = Schema.Schema.Type<typeof CacheExplicit>
export const CachePolicy = Schema.Union([Schema.Literal("auto"), Schema.Literal("none"), CachePolicyObject])
// Omitted configuration uses automatic provider behavior and OpenCode's
// automatic breakpoint placement where required. `"none"` sends no cache key
// or explicit controls; providers may still cache implicitly.
export const CachePolicy = Schema.Union([Schema.Literal("none"), CacheAuto, CacheExplicit])
export type CachePolicy = Schema.Schema.Type<typeof CachePolicy>
export const cacheKey = (cache: CachePolicy | undefined) => (cache && cache !== "none" ? cache.key : undefined)
+10 -10
View File
@@ -64,7 +64,7 @@ describe("applyCachePolicy", () => {
Message.assistant("assistant reply"),
Message.user("latest user message"),
],
cache: "auto",
cache: { mode: "auto" },
}),
)
@@ -93,7 +93,7 @@ describe("applyCachePolicy", () => {
model: openaiModel,
system: "Sys",
prompt: "hi",
cache: "auto",
cache: { mode: "auto" },
}),
)
@@ -112,7 +112,7 @@ describe("applyCachePolicy", () => {
model: geminiModel,
system: "Sys",
prompt: "hi",
cache: "auto",
cache: { mode: "auto" },
}),
)
@@ -133,7 +133,7 @@ describe("applyCachePolicy", () => {
],
tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }],
messages: [Message.user("first user"), Message.assistant("reply"), Message.user("latest user")],
cache: "auto",
cache: { mode: "auto" },
}),
)
@@ -183,7 +183,7 @@ describe("applyCachePolicy", () => {
system: "Sys",
tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }],
prompt: "hi",
cache: { tools: true },
cache: { mode: "explicit", tools: true },
}),
)
@@ -204,7 +204,7 @@ describe("applyCachePolicy", () => {
{ type: "text", text: "last system" },
],
prompt: "hi",
cache: "auto",
cache: { mode: "auto" },
}),
)
@@ -233,7 +233,7 @@ describe("applyCachePolicy", () => {
],
tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }],
prompt: "hi",
cache: "auto",
cache: { mode: "auto" },
})
const applied = applyCachePolicy(request)
expect(applied.tools[0]?.cache).toBeDefined()
@@ -267,7 +267,7 @@ describe("applyCachePolicy", () => {
model: anthropicModel,
system: "Sys",
prompt: "hi",
cache: { system: true, ttlSeconds: 3600 },
cache: { mode: "explicit", system: true, ttlSeconds: 3600 },
}),
)
@@ -283,7 +283,7 @@ describe("applyCachePolicy", () => {
LLM.request({
model: anthropicModel,
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2"), Message.assistant("a2")],
cache: { messages: { tail: 2 } },
cache: { mode: "explicit", messages: { tail: 2 } },
}),
)
@@ -301,7 +301,7 @@ describe("applyCachePolicy", () => {
LLM.request({
model: anthropicModel,
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2")],
cache: { messages: "latest-assistant" },
cache: { mode: "explicit", messages: "latest-assistant" },
}),
)
@@ -3,11 +3,11 @@ import { CloudflareWorkersAI } from "../../src/providers"
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } })
LLM.request({ model, prompt: "Hello", cache: { mode: "auto", key: "cache" } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string.
providerOptions: { openai: { promptCacheKey: 1 } },
// @ts-expect-error Prompt cache keys must be strings.
cache: { mode: "auto", key: 1 },
})
@@ -20,7 +20,7 @@ const cacheRequest = LLM.request({
system: LARGE_CACHEABLE_SYSTEM,
prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 },
providerOptions: { openai: { promptCacheKey: "recorded-cache-test" } },
cache: { mode: "auto", key: "recorded-cache-test" },
})
const recorded = recordedTests({
@@ -680,9 +680,9 @@ describe("OpenAI Responses route", () => {
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "think",
cache: { mode: "auto", key: "session_123" },
providerOptions: {
openai: {
promptCacheKey: "session_123",
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
@@ -801,17 +801,16 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("request OpenAI provider options override route defaults", () =>
it.effect("maps the request prompt cache key", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenAI.configure({
baseURL: "https://api.openai.test/v1/",
apiKey: "test",
providerOptions: { openai: { promptCacheKey: "model_cache" } },
}).model("gpt-4.1-mini"),
prompt: "no cache",
providerOptions: { openai: { promptCacheKey: "request_cache" } },
cache: { mode: "auto", key: "request_cache" },
}),
)
+3 -3
View File
@@ -43,7 +43,7 @@ describe("OpenRouter", () => {
],
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object", properties: {} } }],
prompt: "Hello",
cache: { tools: true, system: true, messages: { tail: 1 } },
cache: { mode: "explicit", tools: true, system: true, messages: { tail: 1 } },
}),
)
@@ -123,7 +123,7 @@ describe("OpenRouter", () => {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
cache: { messages: "latest-assistant" },
cache: { mode: "explicit", messages: "latest-assistant" },
messages: [Message.user("Think"), Message.assistant([{ type: "reasoning", text: "Reasoning" }])],
}),
)
@@ -162,7 +162,6 @@ describe("OpenRouter", () => {
openrouter: {
usage: true,
reasoning: { effort: "high" },
promptCacheKey: "session_123",
models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"],
provider: { order: ["anthropic", "google"], require_parameters: true },
plugins: [{ id: "response-healing" }],
@@ -174,6 +173,7 @@ describe("OpenRouter", () => {
},
}).model("anthropic/claude-3.7-sonnet:thinking"),
prompt: "Think briefly.",
cache: { mode: "auto", key: "session_123" },
}),
)
+1 -1
View File
@@ -108,6 +108,7 @@ function mapOpenRouterOptions(settings: Readonly<Record<string, unknown>>) {
"extraBody",
"fetch",
"headers",
"promptCacheKey",
"timeout",
].includes(key),
),
@@ -124,7 +125,6 @@ function mapXAIOptions(settings: Readonly<Record<string, unknown>>) {
const options = {
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
}
if (Object.keys(options).length === 0) return {}
return { providerOptions: { xai: options } }
+2
View File
@@ -10,6 +10,7 @@ import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import type { SessionMessage } from "./message"
import { SessionModelHeaders } from "./model-headers"
import { SessionPromptCacheKey } from "./prompt-cache-key"
import { App } from "../app"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
@@ -257,6 +258,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: plan.model,
cache: { mode: "auto", key: SessionPromptCacheKey.make(plan.session.id) },
http: { headers: SessionModelHeaders.make(plan.session, dependencies.app) },
messages: [Message.user(plan.prompt)],
tools: [],
+2 -4
View File
@@ -11,6 +11,7 @@ import { SessionContext } from "./context"
import { SessionGenerate } from "./generate"
import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers"
import { SessionPromptCacheKey } from "./prompt-cache-key"
import { SessionRunnerModel } from "./runner/model"
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
import { toLLMMessages } from "./runner/to-llm-message"
@@ -31,9 +32,6 @@ export const layer = Layer.effect(
const model = yield* models.resolve(selection.session)
const history = yield* SessionHistory.preview(database.db, selection.session.id, selection.instructions)
const providerMetadataKey = model.model.route.providerMetadataKey ?? model.model.provider
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id)
? selection.session.id.slice(4)
: selection.session.id
const tools = selection.tools
const toolDefinitions = tools.definitions
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
@@ -71,7 +69,7 @@ export const layer = Layer.effect(
LLM.request({
model: model.model,
http: { headers: SessionModelHeaders.make(selection.session, app) },
providerOptions: { [providerMetadataKey]: { promptCacheKey } },
cache: { mode: "auto", key: SessionPromptCacheKey.make(selection.session.id) },
system: contextEvent.system,
messages: contextEvent.messages,
tools: hookedTools,
+2 -2
View File
@@ -14,6 +14,7 @@ import { QuestionTool } from "../tool/plugin/question"
import { Tool } from "../tool"
import { SessionContext } from "./context"
import { SessionModelHeaders } from "./model-headers"
import { SessionPromptCacheKey } from "./prompt-cache-key"
import { PromptCacheDiagnostics } from "./prompt-cache-diagnostics"
import { MAX_STEPS_PROMPT } from "./runner/max-steps"
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
@@ -183,7 +184,6 @@ export const layer = Layer.effect(
// The final Step keeps definitions available to protocols with native "none",
// preserving their prompt cache prefix. Calls are still rejected at execution.
const tools = input.context.tools
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial]
.filter((part) => part.length > 0)
.map(SystemPart.make)
@@ -213,7 +213,7 @@ export const layer = Layer.effect(
http: {
headers: SessionModelHeaders.make(session, app),
},
providerOptions: { [providerMetadataKey]: { promptCacheKey } },
cache: { mode: "auto", key: SessionPromptCacheKey.make(session.id) },
system: contextEvent.system,
messages: boundImages(unsupportedParts(contextEvent.messages, resolved.capabilities)),
tools: hookedTools,
@@ -0,0 +1,6 @@
export * as SessionPromptCacheKey from "./prompt-cache-key"
import { SessionSchema } from "./schema"
export const make = (sessionID: SessionSchema.ID) =>
/^ses_[0-9a-f]{64}$/.test(sessionID) ? sessionID.slice(4) : sessionID
+2
View File
@@ -12,6 +12,7 @@ import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers"
import { SessionPromptCacheKey } from "./prompt-cache-key"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
import { SessionUsage } from "./usage"
@@ -80,6 +81,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: resolved.model,
cache: { mode: "auto", key: SessionPromptCacheKey.make(session.id) },
http: { headers: SessionModelHeaders.make(session, dependencies.app) },
system: agent.system,
messages: [Message.user(firstUser.text)],
-4
View File
@@ -13,7 +13,6 @@ describe("AISDKNative", () => {
models: ["anthropic/claude-sonnet-4.6"],
provider: { only: ["anthropic"], require_parameters: true },
reasoning: { effort: "high" },
promptCacheKey: "session_123",
future_option: { enabled: true },
}),
).toEqual({
@@ -24,7 +23,6 @@ describe("AISDKNative", () => {
models: ["anthropic/claude-sonnet-4.6"],
provider: { only: ["anthropic"], require_parameters: true },
reasoning: { effort: "high" },
promptCacheKey: "session_123",
future_option: { enabled: true },
},
},
@@ -105,7 +103,6 @@ describe("AISDKNative", () => {
baseURL: "https://xai.example/v1",
reasoningEffort: "custom",
store: true,
promptCacheKey: "cache-key",
}),
).toEqual({
package: "@opencode-ai/ai/providers/xai",
@@ -116,7 +113,6 @@ describe("AISDKNative", () => {
xai: {
reasoningEffort: "custom",
store: true,
promptCacheKey: "cache-key",
},
},
},
@@ -235,6 +235,7 @@ 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]?.cache).toEqual({ mode: "auto", key: sessionID })
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
+1 -1
View File
@@ -287,7 +287,7 @@ it.effect("generates from fresh settled Session context without durable mutation
expect(requests[0]?.system[0]?.text).toBe("Hooked system")
expect(requests[0]?.system.map((part) => part.text)).toContain("Initial context")
expect(requests[0]?.http?.headers).toMatchObject({ "X-Session-Id": sessionID })
expect(requests[0]?.providerOptions).toMatchObject({ openai: { promptCacheKey: sessionID } })
expect(requests[0]?.cache).toEqual({ mode: "auto", key: sessionID })
const instructionUpdates = requests[0]?.messages.flatMap((message) =>
message.role === "system"
? message.content.flatMap((content) => (content.type === "text" ? [content.text] : []))
+2 -2
View File
@@ -3210,7 +3210,7 @@ describe("SessionRunnerLLM", () => {
yield* stream.started
expect(requests).toHaveLength(2)
expect(requests.map((request) => request.providerOptions?.openai?.promptCacheKey)).toEqual([
expect(requests.map((request) => (request.cache !== "none" ? request.cache?.key : undefined))).toEqual([
sessionID,
otherSessionID,
])
@@ -3241,7 +3241,7 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(longSessionID)
yield* session.resume(otherLongSessionID)
const keys = requests.map((request) => request.providerOptions?.openai?.promptCacheKey)
const keys = requests.map((request) => (request.cache !== "none" ? request.cache?.key : undefined))
expect(keys).toEqual([longSessionID.slice(4), otherLongSessionID.slice(4)])
expect(keys.every((key) => typeof key === "string" && key.length === 64)).toBe(true)
expect(keys[0]).not.toBe(keys[1])
+1
View File
@@ -153,6 +153,7 @@ it.effect("generates a title from the sole user message and renames the session"
yield* title.generateForFirstPrompt(sessionID)
expect(requests).toHaveLength(1)
expect(requests[0]?.cache).toEqual({ mode: "auto", key: sessionID })
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,