Compare commits

...

1 Commits

Author SHA1 Message Date
Aiden Cline 751f4ba93f feat(ai): support server-side response compaction 2026-08-20 14:03:33 -05:00
8 changed files with 207 additions and 7 deletions
+36 -3
View File
@@ -69,6 +69,13 @@ const OpenResponsesReasoningItem = Schema.Struct({
encrypted_content: optionalNull(Schema.String),
})
const OpenResponsesCompactionItem = Schema.Struct({
type: Schema.tag("compaction"),
id: Schema.optionalKey(Schema.String),
encrypted_content: Schema.String,
})
type OpenResponsesCompactionItem = Schema.Schema.Type<typeof OpenResponsesCompactionItem>
const OpenResponsesItemReference = Schema.Struct({
type: Schema.tag("item_reference"),
id: Schema.String,
@@ -100,6 +107,7 @@ export const InputItem = Schema.Union([
phase: Schema.optionalKey(MessagePhase),
}),
OpenResponsesReasoningItem,
OpenResponsesCompactionItem,
OpenResponsesItemReference,
Schema.Struct({
type: Schema.tag("function_call"),
@@ -339,6 +347,7 @@ export interface ParserState {
readonly messagePhase: (value: unknown) => MessagePhase | null | undefined
readonly messagePhases: Readonly<Record<string, MessagePhase | null>>
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>
readonly compactionItems: ReadonlyArray<OpenResponsesCompactionItem>
readonly store: boolean | undefined
}
@@ -418,6 +427,12 @@ const hostedToolItemID = (part: ToolResultPart, providerMetadataKey: string) =>
return itemID(part.providerMetadata, providerMetadataKey)
}
const compactionItems = (message: LLMRequest["messages"][number], providerMetadataKey: string) => {
const native = message.native?.[providerMetadataKey]
if (!ProviderShared.isRecord(native) || !Array.isArray(native.compactionItems)) return []
return native.compactionItems.filter(Schema.is(OpenResponsesCompactionItem))
}
const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
part: MediaPart,
request: LLMRequest,
@@ -499,6 +514,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
}
if (message.role === "assistant") {
input.push(...compactionItems(message, providerMetadataKey))
const content: TextPart[] = []
const reasoningItems: Record<string, OpenResponsesReasoningInput> = {}
const reasoningReferences = new Set<string>()
@@ -1029,6 +1045,21 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
] satisfies StepResult
}
if (item.type === "compaction") {
if (!item.id || typeof item.encrypted_content !== "string")
return yield* ProviderShared.eventError(state.id, "Open Responses compaction item is malformed")
return [
{
...state,
compactionItems: [
...state.compactionItems,
{ type: "compaction", id: item.id, encrypted_content: item.encrypted_content },
],
},
NO_EVENTS,
] satisfies StepResult
}
return [state, NO_EVENTS] satisfies StepResult
})
@@ -1049,10 +1080,11 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
},
usage: mapUsage(event.response?.usage, state.providerMetadataKey),
providerMetadata:
event.response?.id || event.response?.service_tier
event.response?.id || event.response?.service_tier || state.compactionItems.length > 0
? providerMetadata(state, {
responseId: event.response.id,
serviceTier: event.response.service_tier,
responseId: event.response?.id,
serviceTier: event.response?.service_tier,
...(state.compactionItems.length > 0 ? { compactionItems: state.compactionItems } : {}),
})
: undefined,
})
@@ -1162,6 +1194,7 @@ export const initial = (request: LLMRequest, extension: Extension = BASE): Parse
messagePhase: (value) => messagePhase(value, extension),
messagePhases: {},
reasoningItems: {},
compactionItems: [],
store: OpenResponsesOptions.resolve(request).store,
})
@@ -13,6 +13,7 @@ import { OpenAIImage } from "./utils/openai-image.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
import { OpenResponsesChannel } from "./open-responses-channel.js"
import { OpenAIResponsesChannel } from "./openai-responses-channel.js"
import { OpenAIOptions } from "./utils/openai-options.js"
const ADAPTER = "openai-responses"
const NAME = "OpenAI Responses"
@@ -56,6 +57,14 @@ const OpenAIResponsesCoreFields = {
input: Schema.Array(OpenAIResponsesInputItem),
tools: optionalArray(OpenAIResponsesTools),
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
context_management: Schema.optional(
Schema.Array(
Schema.Struct({
type: Schema.tag("compaction"),
compact_threshold: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
}),
),
),
}
const OpenAIResponsesBody = Schema.Struct({
@@ -115,6 +124,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
extension,
)
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
const contextManagement = OpenAIOptions.resolve(request).contextManagement
return {
...body,
tools:
@@ -125,6 +135,10 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
),
tool_choice:
body.tool_choice ?? (request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined),
context_management: contextManagement?.map((item) => ({
type: item.type,
compact_threshold: item.compactThreshold,
})),
} satisfies OpenAIResponsesBody
})
@@ -1,3 +1,5 @@
import { Option, Schema } from "effect"
import type { LLMRequest } from "../../schema/index.js"
import { OpenResponsesOptions } from "./open-responses-options.js"
export const OpenAIReasoningEfforts = OpenResponsesOptions.ReasoningEfforts
@@ -19,6 +21,22 @@ export const OpenAIServiceTier = OpenResponsesOptions.ServiceTierSchema
export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort => typeof effort === "string"
export const resolve = OpenResponsesOptions.resolve
export const ContextManagement = Schema.Array(
Schema.Struct({
type: Schema.tag("compaction"),
compactThreshold: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
}),
)
export type ContextManagement = typeof ContextManagement.Type
const Options = Schema.Struct({
contextManagement: Schema.optional(ContextManagement),
})
const decodeOptions = Schema.decodeUnknownOption(Options)
export const resolve = (request: LLMRequest) => ({
...OpenResponsesOptions.resolve(request),
...Option.getOrElse(decodeOptions(request.providerOptions), () => ({})),
})
export * as OpenAIOptions from "./openai-options.js"
+4 -1
View File
@@ -1,9 +1,12 @@
import { mergeProviderOptions, type ProviderOptions } from "../schema/index.js"
import type { OpenResponsesOptionsInput } from "./open-responses-options.js"
import type { ContextManagement } from "../protocols/utils/openai-options.js"
export type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options.js"
export type OpenAIOptionsInput = OpenResponsesOptionsInput
export type OpenAIOptionsInput = OpenResponsesOptionsInput & {
readonly contextManagement?: ContextManagement
}
export type OpenAIProviderOptionsInput = OpenAIOptionsInput
@@ -10,6 +10,11 @@ LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity
LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } })
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "max" } })
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
LLM.request({
model: selected,
prompt: "Hello",
providerOptions: { contextManagement: [{ type: "compaction", compactThreshold: 100_000 }] },
})
LLM.request({
model: selected,
@@ -168,6 +168,50 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("enables server-side compaction", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: {
contextManagement: [{ type: "compaction", compactThreshold: 100_000 }],
},
}),
)
expect(prepared.body.context_management).toEqual([{ type: "compaction", compact_threshold: 100_000 }])
}),
)
it.effect("replays durable server-side compaction items", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.make({
role: "assistant",
content: "After compaction",
native: {
openai: {
compactionItems: [{ type: "compaction", id: "cmp_1", encrypted_content: "opaque-state" }],
},
},
}),
],
}),
)
expect(prepared.body.input).toEqual([
{ type: "compaction", id: "cmp_1", encrypted_content: "opaque-state" },
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "After compaction" }],
},
])
}),
)
it.effect("passes through custom OpenAI reasoning effort strings", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -1506,6 +1550,45 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("retains server-side compaction output for continuation", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{
type: "response.output_item.done",
item: {
type: "compaction",
id: "cmp_1",
encrypted_content: "opaque-state",
status: "completed",
},
},
{ type: "response.completed", response: { id: "resp_1" } },
),
),
),
)
expect(response.events.filter(LLMEvent.is.stepFinish)).toEqual([
{
type: "step-finish",
index: 0,
reason: { normalized: "stop", raw: undefined },
providerMetadata: {
openai: {
responseId: "resp_1",
serviceTier: undefined,
compactionItems: [{ type: "compaction", id: "cmp_1", encrypted_content: "opaque-state" }],
},
},
usage: undefined,
},
])
}),
)
it.effect("preserves standard refusal content as ordinary assistant text", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
@@ -146,6 +146,10 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
const sameProvider = String(message.model.providerID) === String(model.providerID)
const sameModel = sameProvider && String(message.model.id) === String(model.id)
const reuseProviderMetadata = sameModel && message.error === undefined
const native =
reuseProviderMetadata && Array.isArray(message.providerState?.compactionItems)
? { [providerMetadataKey]: message.providerState }
: undefined
const content = message.content.flatMap((item): ContentPart[] => {
if (item.type === "text")
return [
@@ -204,9 +208,15 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
)
.filter((message) => message !== undefined)
.map(Message.tool)
if (meaningful.length === 0) return results
if (meaningful.length === 0 && native === undefined) return results
return [
Message.make({ id: message.id, role: "assistant", content: meaningful, metadata: message.metadata }),
Message.make({
id: message.id,
role: "assistant",
content: meaningful,
metadata: message.metadata,
native,
}),
...results,
]
}
@@ -1019,6 +1019,40 @@ Recent work
])
})
test("carries same-model server compaction state as native message data", () => {
const messages = toLLMMessages(
[
SessionMessage.Assistant.make({
id: id("assistant-compaction"),
type: "assistant",
agent: build,
model,
content: [],
providerState: {
responseId: "resp_1",
compactionItems: [{ type: "compaction", id: "cmp_1", encrypted_content: "opaque-state" }],
},
time: { created, completed: created },
}),
],
model,
)
expect(messages).toEqual([
Message.make({
id: id("assistant-compaction"),
role: "assistant",
content: [],
native: {
provider: {
responseId: "resp_1",
compactionItems: [{ type: "compaction", id: "cmp_1", encrypted_content: "opaque-state" }],
},
},
}),
])
})
test("preserves assistant text provider state across same-provider model changes and failures", () => {
const messages = toLLMMessages(
[