Compare commits

..

6 Commits

Author SHA1 Message Date
Kit Langton 5d37d68bc4 fix(tui): move debug overlay to devtools 2026-08-07 20:33:54 +00:00
opencode-agent[bot] 0b84e24e65 fix(tui): standardize compact terminology (#41141) 2026-08-07 16:26:14 -04:00
opencode-agent[bot] 3776975d5c fix(tui): unify integration connection copy (#41137) 2026-08-07 16:11:51 -04:00
James Long d2c99ba97c chore: improve incremental typecheck performance (#40925)
Co-authored-by: exe.dev user <exedev@jlongster-site.exe.xyz>
2026-08-07 15:34:36 -04:00
Aiden Cline 6f3a3600b9 fix(ai): forward chat cache keys (#41131) 2026-08-07 14:06:37 -05:00
Aiden Cline 9ca650f97c refactor(ai): promote prompt cache key (#39965) 2026-08-07 13:43:00 -05:00
137 changed files with 712 additions and 999 deletions
+4 -3
View File
@@ -368,11 +368,12 @@ Other provider exports listed above remain direct facades until they explicitly
## Provider options & HTTP overlays ## Provider options & HTTP overlays
Three escape hatches in order of stability: Request options in order of stability:
1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop). 1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop).
2. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `promptCacheKey`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing). 2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it.
3. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist. 3. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
4. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist.
Route/provider defaults are overridden by request-level values for each axis. Route/provider defaults are overridden by request-level values for each axis.
+4 -5
View File
@@ -33,9 +33,10 @@ const model = OpenAI.configure({
// //
// - `generation`: common controls such as max tokens, temperature, topP/topK, // - `generation`: common controls such as max tokens, temperature, topP/topK,
// penalties, seed, and stop sequences. // penalties, seed, and stop sequences.
// - `promptCacheKey`: stable cache affinity for protocols that support it.
// - `providerOptions`: namespaced provider-native behavior. For example, // - `providerOptions`: namespaced provider-native behavior. For example,
// OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking // OpenAI store behavior, Anthropic thinking, Gemini thinking config, or
// config, or OpenRouter routing/reasoning. // OpenRouter routing/reasoning.
// - `http`: last-resort serializable overlays for final request body, headers, // - `http`: last-resort serializable overlays for final request body, headers,
// and query params. Prefer typed `providerOptions` when a field is stable. // 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.", system: "You are concise and practical.",
prompt: "Tell me a joke", prompt: "Tell me a joke",
generation: { maxTokens: 80, temperature: 0.7 }, generation: { maxTokens: 80, temperature: 0.7 },
providerOptions: { promptCacheKey: "tutorial-joke",
openai: { promptCacheKey: "tutorial-joke" },
},
}) })
// 3. `generate` sends the request and collects the event stream into one // 3. `generate` sends the request and collects the event stream into one
+57 -118
View File
@@ -90,15 +90,10 @@ const OpenResponsesFunctionCallOutput = Schema.Union([
]) ])
export const InputItem = Schema.Union([ export const InputItem = Schema.Union([
Schema.Struct({ role: Schema.tag("system"), id: Schema.optionalKey(Schema.String), content: Schema.String }), Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
Schema.Struct({ Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }),
role: Schema.tag("user"),
id: Schema.optionalKey(Schema.String),
content: Schema.Array(OpenResponsesInputContent),
}),
Schema.Struct({ Schema.Struct({
role: Schema.tag("assistant"), role: Schema.tag("assistant"),
id: Schema.optionalKey(Schema.String),
content: Schema.Array(OpenResponsesOutputText), content: Schema.Array(OpenResponsesOutputText),
phase: Schema.optionalKey(MessagePhase), phase: Schema.optionalKey(MessagePhase),
}), }),
@@ -106,23 +101,19 @@ export const InputItem = Schema.Union([
OpenResponsesItemReference, OpenResponsesItemReference,
Schema.Struct({ Schema.Struct({
type: Schema.tag("function_call"), type: Schema.tag("function_call"),
id: Schema.optionalKey(Schema.String),
call_id: Schema.String, call_id: Schema.String,
name: Schema.String, name: Schema.String,
arguments: Schema.String, arguments: Schema.String,
}), }),
Schema.Struct({ Schema.Struct({
type: Schema.tag("function_call_output"), type: Schema.tag("function_call_output"),
id: Schema.optionalKey(Schema.String),
call_id: Schema.String, call_id: Schema.String,
output: OpenResponsesFunctionCallOutput, output: OpenResponsesFunctionCallOutput,
}), }),
]) ])
type OpenResponsesInputItem = Schema.Schema.Type<typeof InputItem> type OpenResponsesInputItem = Schema.Schema.Type<typeof InputItem>
type ProviderInputItem = Readonly<Record<string, unknown>> & { readonly type: string; readonly id?: string }
type LoweredInputItem = type LoweredInputItem =
| OpenResponsesInputItem | OpenResponsesInputItem
| ProviderInputItem
| { | {
readonly role: "assistant" readonly role: "assistant"
readonly content: ReadonlyArray<{ readonly type: "output_text"; readonly text: string }> readonly content: ReadonlyArray<{ readonly type: "output_text"; readonly text: string }>
@@ -137,7 +128,7 @@ type OpenResponsesReasoningInput = {
summary: Array<{ type: "summary_text"; text: string }> summary: Array<{ type: "summary_text"; text: string }>
encrypted_content?: string | null encrypted_content?: string | null
} }
type OpenResponsesReasoningReplay = Omit<OpenResponsesReasoningInput, "id"> & { id?: string } type OpenResponsesReasoningReplay = Omit<OpenResponsesReasoningInput, "id">
export const Tool = Schema.Struct({ export const Tool = Schema.Struct({
type: Schema.tag("function"), type: Schema.tag("function"),
@@ -263,11 +254,6 @@ export interface Extension {
readonly request: LLMRequest readonly request: LLMRequest
}) => MediaInput | undefined }) => MediaInput | undefined
readonly messagePhase?: (value: unknown) => MessagePhase | null | undefined readonly messagePhase?: (value: unknown) => MessagePhase | null | undefined
readonly lowerProviderItem?: (
part: ToolResultPart,
providerMetadataKey: string,
store: boolean | undefined,
) => ProviderInputItem | undefined
} }
const BASE: Extension = { id: ADAPTER, name: NAME } const BASE: Extension = { id: ADAPTER, name: NAME }
@@ -324,47 +310,35 @@ export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable<LL
tool: (toolName) => ({ type: "function" as const, name: toolName }), tool: (toolName) => ({ type: "function" as const, name: toolName }),
}) })
const metadataItemID = ( const lowerToolCall = (part: ToolCallPart): OpenResponsesInputItem => ({
part: { readonly itemId?: string; readonly providerMetadata?: ProviderMetadata }, type: "function_call",
providerMetadataKey: string, call_id: part.id,
) => { name: part.name,
if (part.itemId) return part.itemId arguments: ProviderShared.encodeJson(part.input),
const metadata = part.providerMetadata?.[providerMetadataKey] })
return ProviderShared.isRecord(metadata) && typeof metadata.itemId === "string" && metadata.itemId.length > 0
? metadata.itemId
: undefined
}
const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenResponsesInputItem => {
const itemId = metadataItemID(part, providerMetadataKey)
return {
type: "function_call",
...(itemId === undefined ? {} : { id: itemId }),
call_id: part.id,
name: part.name,
arguments: ProviderShared.encodeJson(part.input),
}
}
const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => { const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => {
const metadata = part.providerMetadata?.[providerMetadataKey] const metadata = part.providerMetadata?.[providerMetadataKey]
const itemId = metadataItemID(part, providerMetadataKey) if (!ProviderShared.isRecord(metadata) || typeof metadata.itemId !== "string" || metadata.itemId.length === 0)
if (!itemId) return undefined return undefined
const encryptedContent = const encryptedContent =
ProviderShared.isRecord(metadata) && typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
(typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null)
? metadata.reasoningEncryptedContent ? metadata.reasoningEncryptedContent
: undefined : undefined
return { return {
type: "reasoning", type: "reasoning",
id: itemId, id: metadata.itemId,
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [], summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
encrypted_content: encryptedContent, encrypted_content: encryptedContent,
} }
} }
const hostedToolItemID = (part: ToolResultPart, providerMetadataKey: string) => const hostedToolItemID = (part: ToolResultPart, providerMetadataKey: string) => {
metadataItemID(part, providerMetadataKey) const metadata = part.providerMetadata?.[providerMetadataKey]
return ProviderShared.isRecord(metadata) && typeof metadata.itemId === "string" && metadata.itemId.length > 0
? metadata.itemId
: undefined
}
const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* ( const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
part: MediaPart, part: MediaPart,
@@ -423,18 +397,17 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f
}) })
const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request: LLMRequest, extension: Extension) { const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request: LLMRequest, extension: Extension) {
const options = OpenResponsesOptions.resolve(request)
const system: LoweredInputItem[] = const system: LoweredInputItem[] =
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
const input: LoweredInputItem[] = [...system] const input: LoweredInputItem[] = [...system]
const store = options.store const store = OpenResponsesOptions.resolve(request).store
const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses" const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses"
for (const message of request.messages) { for (const message of request.messages) {
if (message.role === "system") { if (message.role === "system") {
const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message) const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message)
const previous = input.at(-1) const previous = input.at(-1)
if (previous && "role" in previous && previous.role === "user" && Array.isArray(previous.content)) if (previous && "role" in previous && previous.role === "user")
input[input.length - 1] = { input[input.length - 1] = {
role: "user", role: "user",
content: [...previous.content, { type: "input_text", text: part.text }], content: [...previous.content, { type: "input_text", text: part.text }],
@@ -454,24 +427,24 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
if (message.role === "assistant") { if (message.role === "assistant") {
const content: TextPart[] = [] const content: TextPart[] = []
const reasoningItems: Record<string, OpenResponsesReasoningReplay> = {} const reasoningItems: Record<string, OpenResponsesReasoningReplay> = {}
const hostedToolItems = new Set<string>() const reasoningReferences = new Set<string>()
const hostedToolReferences = new Set<string>()
const flushText = () => { const flushText = () => {
if (content.length === 0) return if (content.length === 0) return
const groups = content.reduce< const groups = content.reduce<Array<{ phase: MessagePhase | null | undefined; parts: TextPart[] }>>(
Array<{ phase: MessagePhase | null | undefined; itemId: string | undefined; parts: TextPart[] }> (groups, part) => {
>((groups, part) => { const metadata = part.providerMetadata?.[providerMetadataKey]
const metadata = part.providerMetadata?.[providerMetadataKey] const phase = ProviderShared.isRecord(metadata) ? messagePhase(metadata.phase, extension) : undefined
const phase = ProviderShared.isRecord(metadata) ? messagePhase(metadata.phase, extension) : undefined const group = groups.at(-1)
const itemId = metadataItemID(part, providerMetadataKey) if (group && group.phase === phase) group.parts.push(part)
const group = groups.at(-1) else groups.push({ phase, parts: [part] })
if (group && group.phase === phase && group.itemId === itemId) group.parts.push(part) return groups
else groups.push({ phase, itemId, parts: [part] }) },
return groups [],
}, []) )
input.push( input.push(
...groups.map((group) => ({ ...groups.map((group) => ({
role: "assistant" as const, role: "assistant" as const,
...(group.itemId === undefined ? {} : { id: group.itemId }),
content: group.parts.map((part) => ({ type: "output_text" as const, text: part.text })), content: group.parts.map((part) => ({ type: "output_text" as const, text: part.text })),
...(group.phase === undefined ? {} : { phase: group.phase }), ...(group.phase === undefined ? {} : { phase: group.phase }),
})), })),
@@ -487,6 +460,11 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
flushText() flushText()
const reasoning = lowerReasoning(part, providerMetadataKey) const reasoning = lowerReasoning(part, providerMetadataKey)
if (!reasoning) continue if (!reasoning) continue
if (store !== false) {
if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id })
reasoningReferences.add(reasoning.id)
continue
}
const existing = reasoningItems[reasoning.id] const existing = reasoningItems[reasoning.id]
if (existing) { if (existing) {
existing.summary.push(...reasoning.summary) existing.summary.push(...reasoning.summary)
@@ -496,7 +474,6 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
} }
const replay = { const replay = {
type: reasoning.type, type: reasoning.type,
id: reasoning.id,
summary: reasoning.summary, summary: reasoning.summary,
encrypted_content: reasoning.encrypted_content, encrypted_content: reasoning.encrypted_content,
} }
@@ -507,24 +484,22 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
if (part.type === "tool-call") { if (part.type === "tool-call") {
flushText() flushText()
if (part.providerExecuted === true) continue if (part.providerExecuted === true) continue
input.push(lowerToolCall(part, providerMetadataKey)) input.push(lowerToolCall(part))
continue continue
} }
if (part.type === "tool-result" && part.providerExecuted === true) { if (part.type === "tool-result" && part.providerExecuted === true) {
flushText() flushText()
const itemID = hostedToolItemID(part, providerMetadataKey) const itemID = hostedToolItemID(part, providerMetadataKey)
const providerItem = extension.lowerProviderItem?.(part, providerMetadataKey, store) if (store !== false && itemID && !hostedToolReferences.has(itemID))
if (providerItem && itemID && !hostedToolItems.has(itemID)) input.push(providerItem)
if (!providerItem && store !== false && itemID && !hostedToolItems.has(itemID))
input.push({ type: "item_reference", id: itemID }) input.push({ type: "item_reference", id: itemID })
if (!providerItem && store === false && part.result.type === "content") { if (store === false && part.result.type === "content") {
const content: ReadonlyArray<Content> = part.result.value const content: ReadonlyArray<Content> = part.result.value
input.push({ input.push({
role: "user", role: "user",
content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)), content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)),
}) })
} }
if (itemID) hostedToolItems.add(itemID) if (itemID) hostedToolReferences.add(itemID)
continue continue
} }
return yield* ProviderShared.unsupportedContent(extension.name, "assistant", [ return yield* ProviderShared.unsupportedContent(extension.name, "assistant", [
@@ -564,7 +539,7 @@ const lowerOptions = (request: LLMRequest) => {
return { return {
...(options.instructions ? { instructions: options.instructions } : {}), ...(options.instructions ? { instructions: options.instructions } : {}),
...(options.store !== undefined ? { store: options.store } : {}), ...(options.store !== undefined ? { store: options.store } : {}),
...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}), ...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
...(options.include ? { include: options.include } : {}), ...(options.include ? { include: options.include } : {}),
...(options.reasoningEffort || options.reasoningSummary ...(options.reasoningEffort || options.reasoningSummary
? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } } ? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
@@ -666,9 +641,9 @@ const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepRe
if (!event.delta) return [state, NO_EVENTS] if (!event.delta) return [state, NO_EVENTS]
const events: LLMEvent[] = [] const events: LLMEvent[] = []
const phase = state.messagePhases[id] const phase = state.messagePhases[id]
const metadata = providerMetadata(state, { itemId: id, ...(phase === undefined ? {} : { phase }) }) const metadata = phase === undefined ? undefined : providerMetadata(state, { phase })
const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata, id) const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata)
return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta, metadata, id) }, events] return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) }, events]
} }
const onOutputTextDone = (state: ParserState, event: Event, id: string): StepResult => { const onOutputTextDone = (state: ParserState, event: Event, id: string): StepResult => {
@@ -677,13 +652,7 @@ const onOutputTextDone = (state: ParserState, event: Event, id: string): StepRes
return onOutputTextDelta(state, { ...event, delta: event.text }, id) return onOutputTextDelta(state, { ...event, delta: event.text }, id)
} }
const events: LLMEvent[] = [] const events: LLMEvent[] = []
return [ return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, id) }, events]
{
...state,
lifecycle: Lifecycle.textEnd(state.lifecycle, events, id, providerMetadata(state, { itemId: id }), id),
},
events,
]
} }
export const onReasoningDelta = (state: ParserState, event: Event, itemID: string): StepResult => { export const onReasoningDelta = (state: ParserState, event: Event, itemID: string): StepResult => {
@@ -694,14 +663,7 @@ export const onReasoningDelta = (state: ParserState, event: Event, itemID: strin
return [ return [
{ {
...state, ...state,
lifecycle: Lifecycle.reasoningDelta( lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
state.lifecycle,
events,
id,
event.delta,
providerMetadata(state, { itemId: itemID }),
itemID,
),
}, },
events, events,
] ]
@@ -743,13 +705,7 @@ const onOutputItemAdded = (state: ParserState, event: Event): StepResult => {
return [ return [
{ {
...state, ...state,
lifecycle: Lifecycle.reasoningStart( lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(state, item)),
state.lifecycle,
events,
`${item.id}:0`,
reasoningMetadata(state, item),
item.id,
),
reasoningItems: { reasoningItems: {
...state.reasoningItems, ...state.reasoningItems,
[item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } }, [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
@@ -768,7 +724,6 @@ const onOutputItemAdded = (state: ParserState, event: Event): StepResult => {
lifecycle, lifecycle,
tools: ToolStream.start(state.tools, item.id, { tools: ToolStream.start(state.tools, item.id, {
id: item.call_id ?? item.id, id: item.call_id ?? item.id,
itemId: item.id,
name: item.name ?? "", name: item.name ?? "",
input: item.arguments ?? "", input: item.arguments ?? "",
providerMetadata: metadata, providerMetadata: metadata,
@@ -776,12 +731,7 @@ const onOutputItemAdded = (state: ParserState, event: Event): StepResult => {
}, },
[ [
...events, ...events,
LLMEvent.toolInputStart({ LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata: metadata }),
id: item.call_id ?? item.id,
itemId: item.id,
name: item.name ?? "",
providerMetadata: metadata,
}),
], ],
] ]
} }
@@ -800,7 +750,6 @@ const onReasoningSummaryPartAdded = (state: ParserState, event: Event): StepResu
events, events,
`${event.item_id}:0`, `${event.item_id}:0`,
providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: null }), providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: null }),
event.item_id,
), ),
reasoningItems: { reasoningItems: {
...state.reasoningItems, ...state.reasoningItems,
@@ -821,7 +770,6 @@ const onReasoningSummaryPartAdded = (state: ParserState, event: Event): StepResu
events, events,
`${event.item_id}:${entry[0]}`, `${event.item_id}:${entry[0]}`,
providerMetadata(state, { itemId: event.item_id }), providerMetadata(state, { itemId: event.item_id }),
event.item_id,
), ),
state.lifecycle, state.lifecycle,
) )
@@ -833,7 +781,6 @@ const onReasoningSummaryPartAdded = (state: ParserState, event: Event): StepResu
events, events,
`${event.item_id}:${event.summary_index}`, `${event.item_id}:${event.summary_index}`,
providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }), providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }),
event.item_id,
), ),
reasoningItems: { reasoningItems: {
...state.reasoningItems, ...state.reasoningItems,
@@ -869,7 +816,6 @@ const onReasoningSummaryPartDone = (state: ParserState, event: Event): StepResul
events, events,
`${event.item_id}:${event.summary_index}`, `${event.item_id}:${event.summary_index}`,
providerMetadata(state, { itemId: event.item_id }), providerMetadata(state, { itemId: event.item_id }),
event.item_id,
) )
: state.lifecycle, : state.lifecycle,
reasoningItems: { reasoningItems: {
@@ -924,8 +870,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
state.lifecycle, state.lifecycle,
events, events,
item.id, item.id,
providerMetadata(state, { itemId: item.id, ...(phase === undefined ? {} : { phase }) }), phase === undefined ? undefined : providerMetadata(state, { phase }),
item.id,
), ),
messageItems, messageItems,
messagePhases, messagePhases,
@@ -936,15 +881,9 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
if (item.type === "function_call") { if (item.type === "function_call") {
if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult
const metadata = providerMetadata(state, { itemId: item.id })
const tools = state.tools[item.id] const tools = state.tools[item.id]
? state.tools ? state.tools
: ToolStream.start(state.tools, item.id, { : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name })
id: item.call_id,
itemId: item.id,
name: item.name,
providerMetadata: metadata,
})
const result = const result =
item.arguments === undefined item.arguments === undefined
? yield* ToolStream.finish(state.id, tools, item.id) ? yield* ToolStream.finish(state.id, tools, item.id)
@@ -974,7 +913,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
const lifecycle = Object.entries(reasoningItem.summaryParts) const lifecycle = Object.entries(reasoningItem.summaryParts)
.filter((entry) => entry[1] === "active" || entry[1] === "can-conclude") .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
.reduce( .reduce(
(lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, metadata, item.id), (lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, metadata),
state.lifecycle, state.lifecycle,
) )
const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems
@@ -982,12 +921,12 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
} }
if (!state.lifecycle.reasoning.has(item.id)) { if (!state.lifecycle.reasoning.has(item.id)) {
const lifecycle = Lifecycle.stepStart(state.lifecycle, events) const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
events.push(LLMEvent.reasoningStart({ id: item.id, itemId: item.id, providerMetadata: metadata })) events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata: metadata }))
events.push(LLMEvent.reasoningEnd({ id: item.id, itemId: item.id, providerMetadata: metadata })) events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata: metadata }))
return [{ ...state, lifecycle }, events] satisfies StepResult return [{ ...state, lifecycle }, events] satisfies StepResult
} }
return [ return [
{ ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, metadata, item.id) }, { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, metadata) },
events, events,
] satisfies StepResult ] satisfies StepResult
} }
+2
View File
@@ -132,6 +132,7 @@ export const bodyFields = {
stream: Schema.Literal(true), stream: Schema.Literal(true),
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })), stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
store: Schema.optional(Schema.Boolean), store: Schema.optional(Schema.Boolean),
prompt_cache_key: Schema.optional(Schema.String),
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
max_completion_tokens: Schema.optional(Schema.Number), max_completion_tokens: Schema.optional(Schema.Number),
max_tokens: Schema.optional(Schema.Number), max_tokens: Schema.optional(Schema.Number),
@@ -509,6 +510,7 @@ const lowerOptions = (request: LLMRequest) => {
const options = OpenAIOptions.resolve(request) const options = OpenAIOptions.resolve(request)
return { return {
...(options.store !== undefined ? { store: options.store } : {}), ...(options.store !== undefined ? { store: options.store } : {}),
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}), ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
} }
} }
+3 -32
View File
@@ -38,14 +38,10 @@ const OpenAIResponsesToolChoice = Schema.Union([
const OpenAIResponsesInputItem = Schema.Union([ const OpenAIResponsesInputItem = Schema.Union([
Schema.Struct({ Schema.Struct({
role: Schema.tag("assistant"), role: Schema.tag("assistant"),
id: Schema.optionalKey(Schema.String),
content: Schema.Array(Schema.Struct({ type: Schema.tag("output_text"), text: Schema.String })), content: Schema.Array(Schema.Struct({ type: Schema.tag("output_text"), text: Schema.String })),
phase: Schema.optionalKey(Schema.NullOr(OpenResponses.MessagePhase)), phase: Schema.optionalKey(Schema.NullOr(OpenResponses.MessagePhase)),
}), }),
OpenResponses.InputItem, OpenResponses.InputItem,
Schema.StructWithRest(Schema.Struct({ type: Schema.String, id: Schema.optionalKey(Schema.String) }), [
Schema.Record(Schema.String, Schema.Unknown),
]),
]) ])
const OpenAIResponsesCoreFields = { const OpenAIResponsesCoreFields = {
@@ -84,25 +80,6 @@ const extension = {
mime_type: media.mime, mime_type: media.mime,
} }
}, },
lowerProviderItem: (part, providerMetadataKey, store) => {
const metadata = part.providerMetadata?.[providerMetadataKey]
if (!ProviderShared.isRecord(metadata) || !ProviderShared.isRecord(metadata.item)) return undefined
if (typeof metadata.item.type !== "string") return undefined
const id = typeof metadata.item.id === "string" ? metadata.item.id : undefined
// The public API requires stored state to replay image-generation items. In
// stateless mode, lower the generated file through the existing user-image fallback.
if (metadata.item.type === "image_generation_call" && store === false) return undefined
if (metadata.item.type === "image_generation_call")
return {
type: metadata.item.type,
...(id === undefined ? {} : { id }),
...(typeof metadata.item.status === "string" ? { status: metadata.item.status } : {}),
...(typeof metadata.item.revised_prompt === "string" ? { revised_prompt: metadata.item.revised_prompt } : {}),
...(typeof metadata.item.result === "string" ? { result: metadata.item.result } : {}),
}
const item: Record<string, unknown> & { type: string } = { ...metadata.item, type: metadata.item.type }
return item
},
} satisfies OpenResponses.Extension } satisfies OpenResponses.Extension
const nativeImageToolInput = (tool: ToolDefinition) => { const nativeImageToolInput = (tool: ToolDefinition) => {
@@ -218,29 +195,23 @@ const onHostedToolDone = Effect.fn("OpenAIResponses.onHostedToolDone")(function*
item: HostedToolItem, item: HostedToolItem,
) { ) {
const tool = HOSTED_TOOLS[item.type] const tool = HOSTED_TOOLS[item.type]
const callMetadata = OpenResponses.providerMetadata(state, { itemId: item.id }) const providerMetadata = OpenResponses.providerMetadata(state, { itemId: item.id })
const resultMetadata = OpenResponses.providerMetadata(
state,
item.type === "image_generation_call" ? { itemId: item.id } : { itemId: item.id, item },
)
const events: LLMEvent[] = [] const events: LLMEvent[] = []
const lifecycle = Lifecycle.stepStart(state.lifecycle, events) const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
events.push( events.push(
LLMEvent.toolCall({ LLMEvent.toolCall({
id: item.id, id: item.id,
itemId: item.id,
name: tool.name, name: tool.name,
input: tool.input(item), input: tool.input(item),
providerExecuted: true, providerExecuted: true,
providerMetadata: callMetadata, providerMetadata,
}), }),
LLMEvent.toolResult({ LLMEvent.toolResult({
id: item.id, id: item.id,
itemId: item.id,
name: tool.name, name: tool.name,
result: yield* hostedToolResult(item), result: yield* hostedToolResult(item),
providerExecuted: true, providerExecuted: true,
providerMetadata: resultMetadata, providerMetadata,
}), }),
) )
return [{ ...state, lifecycle }, events] satisfies OpenResponses.StepResult return [{ ...state, lifecycle }, events] satisfies OpenResponses.StepResult
+12 -40
View File
@@ -1,10 +1,4 @@
import { import { LLMEvent, type FinishReasonDetails, type ProviderMetadata, type Usage } from "../../schema"
LLMEvent,
type FinishReasonDetails,
type ProviderMetadata,
type ResponseItemID,
type Usage,
} from "../../schema"
export interface State { export interface State {
readonly stepStarted: boolean readonly stepStarted: boolean
@@ -20,29 +14,16 @@ export const stepStart = (state: State, events: LLMEvent[]): State => {
return { ...state, stepStarted: true } return { ...state, stepStarted: true }
} }
export const textStart = ( export const textStart = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => {
state: State,
events: LLMEvent[],
id: string,
providerMetadata?: ProviderMetadata,
itemId?: ResponseItemID,
): State => {
if (state.text.has(id)) return state if (state.text.has(id)) return state
const stepped = stepStart(state, events) const stepped = stepStart(state, events)
events.push(LLMEvent.textStart({ id, ...(itemId === undefined ? {} : { itemId }), providerMetadata })) events.push(LLMEvent.textStart({ id, providerMetadata }))
return { ...stepped, text: new Set([...stepped.text, id]) } return { ...stepped, text: new Set([...stepped.text, id]) }
} }
export const textDelta = ( export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
state: State, const started = textStart(state, events, id)
events: LLMEvent[], events.push(LLMEvent.textDelta({ id, text }))
id: string,
text: string,
providerMetadata?: ProviderMetadata,
itemId?: ResponseItemID,
): State => {
const started = textStart(state, events, id, providerMetadata, itemId)
events.push(LLMEvent.textDelta({ id, ...(itemId === undefined ? {} : { itemId }), text, providerMetadata }))
return started return started
} }
@@ -51,11 +32,10 @@ export const reasoningStart = (
events: LLMEvent[], events: LLMEvent[],
id: string, id: string,
providerMetadata?: ProviderMetadata, providerMetadata?: ProviderMetadata,
itemId?: ResponseItemID,
): State => { ): State => {
if (state.reasoning.has(id)) return state if (state.reasoning.has(id)) return state
const stepped = stepStart(state, events) const stepped = stepStart(state, events)
events.push(LLMEvent.reasoningStart({ id, ...(itemId === undefined ? {} : { itemId }), providerMetadata })) events.push(LLMEvent.reasoningStart({ id, providerMetadata }))
return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) } return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) }
} }
@@ -65,10 +45,9 @@ export const reasoningDelta = (
id: string, id: string,
text: string, text: string,
providerMetadata?: ProviderMetadata, providerMetadata?: ProviderMetadata,
itemId?: ResponseItemID,
): State => { ): State => {
const started = reasoningStart(state, events, id, providerMetadata, itemId) const started = reasoningStart(state, events, id, providerMetadata)
events.push(LLMEvent.reasoningDelta({ id, ...(itemId === undefined ? {} : { itemId }), text, providerMetadata })) events.push(LLMEvent.reasoningDelta({ id, text, providerMetadata }))
return started return started
} }
@@ -77,26 +56,19 @@ export const reasoningEnd = (
events: LLMEvent[], events: LLMEvent[],
id: string, id: string,
providerMetadata?: ProviderMetadata, providerMetadata?: ProviderMetadata,
itemId?: ResponseItemID,
): State => { ): State => {
if (!state.reasoning.has(id)) return state if (!state.reasoning.has(id)) return state
const stepped = stepStart(state, events) const stepped = stepStart(state, events)
events.push(LLMEvent.reasoningEnd({ id, ...(itemId === undefined ? {} : { itemId }), providerMetadata })) events.push(LLMEvent.reasoningEnd({ id, providerMetadata }))
const reasoning = new Set(stepped.reasoning) const reasoning = new Set(stepped.reasoning)
reasoning.delete(id) reasoning.delete(id)
return { ...stepped, reasoning } return { ...stepped, reasoning }
} }
export const textEnd = ( export const textEnd = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => {
state: State,
events: LLMEvent[],
id: string,
providerMetadata?: ProviderMetadata,
itemId?: ResponseItemID,
): State => {
if (!state.text.has(id)) return state if (!state.text.has(id)) return state
const stepped = stepStart(state, events) const stepped = stepStart(state, events)
events.push(LLMEvent.textEnd({ id, ...(itemId === undefined ? {} : { itemId }), providerMetadata })) events.push(LLMEvent.textEnd({ id, providerMetadata }))
const text = new Set(stepped.text) const text = new Set(stepped.text)
text.delete(id) text.delete(id)
return { ...stepped, text } return { ...stepped, text }
@@ -33,7 +33,6 @@ export const ServiceTierSchema = Schema.Literals(ServiceTiers)
export interface Resolved { export interface Resolved {
readonly instructions?: string readonly instructions?: string
readonly store?: boolean readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: string readonly reasoningEffort?: string
readonly reasoningSummary?: "auto" | "concise" | "detailed" readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable> readonly include?: ReadonlyArray<ResponseIncludable>
@@ -50,7 +49,6 @@ export const resolve = (request: LLMRequest): Resolved => {
return { return {
instructions: typeof input?.instructions === "string" ? input.instructions : undefined, instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
store: typeof input?.store === "boolean" ? input.store : 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, reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
reasoningSummary: reasoningSummary:
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed" reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
+2 -23
View File
@@ -1,12 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { import { AIError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema"
AIError,
LLMEvent,
type ProviderMetadata,
type ResponseItemID,
type ToolCall,
type ToolInputError,
} from "../../schema"
import { eventError, parseToolInput, type ToolAccumulator } from "../shared" import { eventError, parseToolInput, type ToolAccumulator } from "../shared"
type StreamKey = string | number type StreamKey = string | number
@@ -17,7 +10,6 @@ type StreamKey = string | number
* so far, not the parsed object. * so far, not the parsed object.
*/ */
export interface PendingTool extends ToolAccumulator { export interface PendingTool extends ToolAccumulator {
readonly itemId?: ResponseItemID
readonly providerExecuted?: boolean readonly providerExecuted?: boolean
readonly providerMetadata?: ProviderMetadata readonly providerMetadata?: ProviderMetadata
} }
@@ -60,7 +52,6 @@ const withoutTool = <K extends StreamKey>(tools: State<K>, key: K): State<K> =>
const inputStart = (tool: PendingTool) => const inputStart = (tool: PendingTool) =>
LLMEvent.toolInputStart({ LLMEvent.toolInputStart({
id: tool.id, id: tool.id,
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
name: tool.name, name: tool.name,
providerExecuted: tool.providerExecuted ? true : undefined, providerExecuted: tool.providerExecuted ? true : undefined,
providerMetadata: tool.providerMetadata, providerMetadata: tool.providerMetadata,
@@ -69,7 +60,6 @@ const inputStart = (tool: PendingTool) =>
const inputDelta = (tool: PendingTool, text: string) => const inputDelta = (tool: PendingTool, text: string) =>
LLMEvent.toolInputDelta({ LLMEvent.toolInputDelta({
id: tool.id, id: tool.id,
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
name: tool.name, name: tool.name,
text, text,
}) })
@@ -80,7 +70,6 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
Effect.map((input): ToolCall | ToolInputError => Effect.map((input): ToolCall | ToolInputError =>
LLMEvent.toolCall({ LLMEvent.toolCall({
id: tool.id, id: tool.id,
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
name: tool.name, name: tool.name,
input, input,
providerExecuted: tool.providerExecuted ? true : undefined, providerExecuted: tool.providerExecuted ? true : undefined,
@@ -93,7 +82,6 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
: Effect.succeed( : Effect.succeed(
LLMEvent.toolInputError({ LLMEvent.toolInputError({
id: tool.id, id: tool.id,
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
name: tool.name, name: tool.name,
raw, raw,
}), }),
@@ -105,15 +93,7 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
const finishEvents = (tool: PendingTool, event: ToolCall | ToolInputError): ReadonlyArray<LLMEvent> => const finishEvents = (tool: PendingTool, event: ToolCall | ToolInputError): ReadonlyArray<LLMEvent> =>
event.type === "tool-input-error" event.type === "tool-input-error"
? [event] ? [event]
: [ : [LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }), event]
LLMEvent.toolInputEnd({
id: tool.id,
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
name: tool.name,
providerMetadata: tool.providerMetadata,
}),
event,
]
/** Store the updated tool and produce the optional public delta event. */ /** Store the updated tool and produce the optional public delta event. */
const appendTool = <K extends StreamKey>( const appendTool = <K extends StreamKey>(
@@ -168,7 +148,6 @@ export const appendOrStart = <K extends StreamKey>(
id, id,
name, name,
input: `${current?.input ?? ""}${delta.text}`, input: `${current?.input ?? ""}${delta.text}`,
itemId: current?.itemId,
providerExecuted: current?.providerExecuted, providerExecuted: current?.providerExecuted,
providerMetadata: current?.providerMetadata, providerMetadata: current?.providerMetadata,
} }
@@ -5,7 +5,6 @@ export interface OpenResponsesOptionsInput {
readonly [key: string]: unknown readonly [key: string]: unknown
readonly instructions?: string readonly instructions?: string
readonly store?: boolean readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: ReasoningEffort readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: "auto" | "concise" | "detailed" readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable> readonly include?: ReadonlyArray<ResponseIncludable>
@@ -17,7 +17,6 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
const openai = Object.fromEntries( const openai = Object.fromEntries(
definedEntries({ definedEntries({
store: options?.store, store: options?.store,
promptCacheKey: options?.promptCacheKey,
reasoningEffort: options?.reasoningEffort, reasoningEffort: options?.reasoningEffort,
reasoningSummary: options?.reasoningSummary, reasoningSummary: options?.reasoningSummary,
include: options?.include, include: options?.include,
+1 -2
View File
@@ -55,7 +55,6 @@ export interface OpenRouterOptions {
readonly debug?: Readonly<{ echo_upstream_body?: boolean }> readonly debug?: Readonly<{ echo_upstream_body?: boolean }>
readonly models?: ReadonlyArray<string> readonly models?: ReadonlyArray<string>
readonly plugins?: ReadonlyArray<OpenRouterPlugin> readonly plugins?: ReadonlyArray<OpenRouterPlugin>
readonly promptCacheKey?: string
readonly provider?: OpenRouterProviderRouting readonly provider?: OpenRouterProviderRouting
readonly reasoning?: Readonly<{ readonly reasoning?: Readonly<{
enabled?: boolean enabled?: boolean
@@ -122,6 +121,7 @@ export const protocol = Protocol.make({
...body, ...body,
messages, messages,
...bodyOptions(request.providerOptions?.openrouter), ...bodyOptions(request.providerOptions?.openrouter),
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
} as OpenRouterBody } as OpenRouterBody
}), }),
), ),
@@ -161,7 +161,6 @@ const bodyOptions = (input: unknown) => {
...(isRecord(debug) ? { debug } : {}), ...(isRecord(debug) ? { debug } : {}),
...(typeof user === "string" ? { user } : {}), ...(typeof user === "string" ? { user } : {}),
...(isRecord(reasoning) ? { reasoning } : {}), ...(isRecord(reasoning) ? { reasoning } : {}),
...(typeof promptCacheKey === "string" ? { prompt_cache_key: promptCacheKey } : {}),
} }
} }
+2
View File
@@ -47,6 +47,8 @@ const chatRoute = Route.make({
protocol: OpenAIChat.protocol, protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }), endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }),
transport: OpenAICompatibleChat.route.transport, transport: OpenAICompatibleChat.route.transport,
headers: ({ request }): Record<string, string> =>
request.promptCacheKey ? { "x-grok-conv-id": request.promptCacheKey } : {},
}) })
export const routes = [responsesRoute, chatRoute] export const routes = [responsesRoute, chatRoute]
+24 -72
View File
@@ -1,5 +1,5 @@
import { Schema } from "effect" import { Schema } from "effect"
import { ContentBlockID, FinishReason, ProviderMetadata, ResponseItemID, ToolCallID } from "./ids" import { ContentBlockID, FinishReason, ProviderMetadata, ToolCallID } from "./ids"
import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages" import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages"
import { ProviderFailureClassification } from "./errors" import { ProviderFailureClassification } from "./errors"
@@ -84,7 +84,6 @@ export type StepStart = Schema.Schema.Type<typeof StepStart>
export const TextStart = Schema.Struct({ export const TextStart = Schema.Struct({
type: Schema.tag("text-start"), type: Schema.tag("text-start"),
id: ContentBlockID, id: ContentBlockID,
itemId: Schema.optional(ResponseItemID),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.TextStart" }) }).annotate({ identifier: "LLM.Event.TextStart" })
export type TextStart = Schema.Schema.Type<typeof TextStart> export type TextStart = Schema.Schema.Type<typeof TextStart>
@@ -93,7 +92,6 @@ export const TextDelta = Schema.Struct({
type: Schema.tag("text-delta"), type: Schema.tag("text-delta"),
id: ContentBlockID, id: ContentBlockID,
text: Schema.String, text: Schema.String,
itemId: Schema.optional(ResponseItemID),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.TextDelta" }) }).annotate({ identifier: "LLM.Event.TextDelta" })
export type TextDelta = Schema.Schema.Type<typeof TextDelta> export type TextDelta = Schema.Schema.Type<typeof TextDelta>
@@ -101,7 +99,6 @@ export type TextDelta = Schema.Schema.Type<typeof TextDelta>
export const TextEnd = Schema.Struct({ export const TextEnd = Schema.Struct({
type: Schema.tag("text-end"), type: Schema.tag("text-end"),
id: ContentBlockID, id: ContentBlockID,
itemId: Schema.optional(ResponseItemID),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.TextEnd" }) }).annotate({ identifier: "LLM.Event.TextEnd" })
export type TextEnd = Schema.Schema.Type<typeof TextEnd> export type TextEnd = Schema.Schema.Type<typeof TextEnd>
@@ -109,7 +106,6 @@ export type TextEnd = Schema.Schema.Type<typeof TextEnd>
export const ReasoningStart = Schema.Struct({ export const ReasoningStart = Schema.Struct({
type: Schema.tag("reasoning-start"), type: Schema.tag("reasoning-start"),
id: ContentBlockID, id: ContentBlockID,
itemId: Schema.optional(ResponseItemID),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ReasoningStart" }) }).annotate({ identifier: "LLM.Event.ReasoningStart" })
export type ReasoningStart = Schema.Schema.Type<typeof ReasoningStart> export type ReasoningStart = Schema.Schema.Type<typeof ReasoningStart>
@@ -118,7 +114,6 @@ export const ReasoningDelta = Schema.Struct({
type: Schema.tag("reasoning-delta"), type: Schema.tag("reasoning-delta"),
id: ContentBlockID, id: ContentBlockID,
text: Schema.String, text: Schema.String,
itemId: Schema.optional(ResponseItemID),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ReasoningDelta" }) }).annotate({ identifier: "LLM.Event.ReasoningDelta" })
export type ReasoningDelta = Schema.Schema.Type<typeof ReasoningDelta> export type ReasoningDelta = Schema.Schema.Type<typeof ReasoningDelta>
@@ -126,7 +121,6 @@ export type ReasoningDelta = Schema.Schema.Type<typeof ReasoningDelta>
export const ReasoningEnd = Schema.Struct({ export const ReasoningEnd = Schema.Struct({
type: Schema.tag("reasoning-end"), type: Schema.tag("reasoning-end"),
id: ContentBlockID, id: ContentBlockID,
itemId: Schema.optional(ResponseItemID),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ReasoningEnd" }) }).annotate({ identifier: "LLM.Event.ReasoningEnd" })
export type ReasoningEnd = Schema.Schema.Type<typeof ReasoningEnd> export type ReasoningEnd = Schema.Schema.Type<typeof ReasoningEnd>
@@ -135,7 +129,6 @@ export const ToolInputStart = Schema.Struct({
type: Schema.tag("tool-input-start"), type: Schema.tag("tool-input-start"),
id: ToolCallID, id: ToolCallID,
name: Schema.String, name: Schema.String,
itemId: Schema.optional(ResponseItemID),
providerExecuted: Schema.optional(Schema.Boolean), providerExecuted: Schema.optional(Schema.Boolean),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ToolInputStart" }) }).annotate({ identifier: "LLM.Event.ToolInputStart" })
@@ -144,7 +137,6 @@ export type ToolInputStart = Schema.Schema.Type<typeof ToolInputStart>
export const ToolInputDelta = Schema.Struct({ export const ToolInputDelta = Schema.Struct({
type: Schema.tag("tool-input-delta"), type: Schema.tag("tool-input-delta"),
id: ToolCallID, id: ToolCallID,
itemId: Schema.optional(ResponseItemID),
name: Schema.String, name: Schema.String,
text: Schema.String, text: Schema.String,
}).annotate({ identifier: "LLM.Event.ToolInputDelta" }) }).annotate({ identifier: "LLM.Event.ToolInputDelta" })
@@ -154,7 +146,6 @@ export const ToolInputEnd = Schema.Struct({
type: Schema.tag("tool-input-end"), type: Schema.tag("tool-input-end"),
id: ToolCallID, id: ToolCallID,
name: Schema.String, name: Schema.String,
itemId: Schema.optional(ResponseItemID),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ToolInputEnd" }) }).annotate({ identifier: "LLM.Event.ToolInputEnd" })
export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd> export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd>
@@ -163,7 +154,6 @@ export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd>
export const ToolInputError = Schema.Struct({ export const ToolInputError = Schema.Struct({
type: Schema.tag("tool-input-error"), type: Schema.tag("tool-input-error"),
id: ToolCallID, id: ToolCallID,
itemId: Schema.optional(ResponseItemID),
name: Schema.String, name: Schema.String,
raw: Schema.String, raw: Schema.String,
}).annotate({ identifier: "LLM.Event.ToolInputError" }) }).annotate({ identifier: "LLM.Event.ToolInputError" })
@@ -172,7 +162,6 @@ export type ToolInputError = Schema.Schema.Type<typeof ToolInputError>
export const ToolCall = Schema.Struct({ export const ToolCall = Schema.Struct({
type: Schema.tag("tool-call"), type: Schema.tag("tool-call"),
id: ToolCallID, id: ToolCallID,
itemId: Schema.optional(ResponseItemID),
name: Schema.String, name: Schema.String,
input: Schema.Unknown, input: Schema.Unknown,
providerExecuted: Schema.optional(Schema.Boolean), providerExecuted: Schema.optional(Schema.Boolean),
@@ -183,7 +172,6 @@ export type ToolCall = Schema.Schema.Type<typeof ToolCall>
export const ToolResult = Schema.Struct({ export const ToolResult = Schema.Struct({
type: Schema.tag("tool-result"), type: Schema.tag("tool-result"),
id: ToolCallID, id: ToolCallID,
itemId: Schema.optional(ResponseItemID),
name: Schema.String, name: Schema.String,
result: ToolResultValue, result: ToolResultValue,
output: Schema.optional(ToolOutput), output: Schema.optional(ToolOutput),
@@ -195,7 +183,6 @@ export type ToolResult = Schema.Schema.Type<typeof ToolResult>
export const ToolError = Schema.Struct({ export const ToolError = Schema.Struct({
type: Schema.tag("tool-error"), type: Schema.tag("tool-error"),
id: ToolCallID, id: ToolCallID,
itemId: Schema.optional(ResponseItemID),
name: Schema.String, name: Schema.String,
message: Schema.String, message: Schema.String,
error: Schema.optional(Schema.Defect()), error: Schema.optional(Schema.Defect()),
@@ -347,14 +334,12 @@ const responseUsage = (events: ReadonlyArray<LLMEvent>) =>
interface ContentAssembly { interface ContentAssembly {
readonly contentIndex: number readonly contentIndex: number
readonly text: string readonly text: string
readonly itemId?: ResponseItemID
readonly providerMetadata?: ProviderMetadata readonly providerMetadata?: ProviderMetadata
} }
interface ToolInputAssembly { interface ToolInputAssembly {
readonly name: string readonly name: string
readonly text: string readonly text: string
readonly itemId?: ResponseItemID
readonly providerMetadata?: ProviderMetadata readonly providerMetadata?: ProviderMetadata
} }
@@ -400,27 +385,11 @@ const appendEvent = (state: ResponseState, event: LLMEvent): ResponseState => {
} }
} }
const textContent = ( const textContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart =>
text: string, providerMetadata === undefined ? { type: "text", text } : { type: "text", text, providerMetadata }
itemId: ResponseItemID | undefined,
providerMetadata: ProviderMetadata | undefined,
): ContentPart => ({
type: "text",
text,
...(itemId === undefined ? {} : { itemId }),
...(providerMetadata === undefined ? {} : { providerMetadata }),
})
const reasoningContent = ( const reasoningContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart =>
text: string, providerMetadata === undefined ? { type: "reasoning", text } : { type: "reasoning", text, providerMetadata }
itemId: ResponseItemID | undefined,
providerMetadata: ProviderMetadata | undefined,
): ContentPart => ({
type: "reasoning",
text,
...(itemId === undefined ? {} : { itemId }),
...(providerMetadata === undefined ? {} : { providerMetadata }),
})
const contentWith = (state: ResponseState, content: ReadonlyArray<ContentPart>): ResponseState => ({ const contentWith = (state: ResponseState, content: ReadonlyArray<ContentPart>): ResponseState => ({
...state, ...state,
@@ -435,32 +404,26 @@ const replaceContent = (state: ResponseState, index: number, part: ContentPart)
state.message.content.map((item, itemIndex) => (itemIndex === index ? part : item)), state.message.content.map((item, itemIndex) => (itemIndex === index ? part : item)),
) )
const ensureText = ( const ensureText = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => {
state: ResponseState,
id: string,
itemId?: ResponseItemID,
providerMetadata?: ProviderMetadata,
): ResponseState => {
if (state.textParts[id]) return state if (state.textParts[id]) return state
return { return {
...appendContent(state, textContent("", itemId, providerMetadata)), ...appendContent(state, textContent("", providerMetadata)),
textParts: { textParts: {
...state.textParts, ...state.textParts,
[id]: { contentIndex: state.message.content.length, text: "", itemId, providerMetadata }, [id]: { contentIndex: state.message.content.length, text: "", providerMetadata },
}, },
} }
} }
const reduceTextDelta = (state: ResponseState, event: TextDelta): ResponseState => { const reduceTextDelta = (state: ResponseState, event: TextDelta): ResponseState => {
const started = ensureText(state, event.id, event.itemId, event.providerMetadata) const started = ensureText(state, event.id, event.providerMetadata)
const current = started.textParts[event.id] const current = started.textParts[event.id]
if (!current) return started if (!current) return started
const text = current.text + event.text const text = current.text + event.text
const itemId = event.itemId ?? current.itemId
const providerMetadata = event.providerMetadata ?? current.providerMetadata const providerMetadata = event.providerMetadata ?? current.providerMetadata
return { return {
...replaceContent(started, current.contentIndex, textContent(text, itemId, providerMetadata)), ...replaceContent(started, current.contentIndex, textContent(text, providerMetadata)),
textParts: { ...started.textParts, [event.id]: { ...current, text, itemId, providerMetadata } }, textParts: { ...started.textParts, [event.id]: { ...current, text, providerMetadata } },
} }
} }
@@ -468,39 +431,32 @@ const reduceTextEnd = (state: ResponseState, event: TextEnd): ResponseState => {
const current = state.textParts[event.id] const current = state.textParts[event.id]
if (!current) return state if (!current) return state
const providerMetadata = event.providerMetadata ?? current.providerMetadata const providerMetadata = event.providerMetadata ?? current.providerMetadata
const itemId = event.itemId ?? current.itemId
return { return {
...replaceContent(state, current.contentIndex, textContent(current.text, itemId, providerMetadata)), ...replaceContent(state, current.contentIndex, textContent(current.text, providerMetadata)),
textParts: { ...state.textParts, [event.id]: { ...current, itemId, providerMetadata } }, textParts: { ...state.textParts, [event.id]: { ...current, providerMetadata } },
} }
} }
const ensureReasoning = ( const ensureReasoning = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => {
state: ResponseState,
id: string,
itemId?: ResponseItemID,
providerMetadata?: ProviderMetadata,
): ResponseState => {
if (state.reasoningParts[id]) return state if (state.reasoningParts[id]) return state
return { return {
...appendContent(state, reasoningContent("", itemId, providerMetadata)), ...appendContent(state, reasoningContent("", providerMetadata)),
reasoningParts: { reasoningParts: {
...state.reasoningParts, ...state.reasoningParts,
[id]: { contentIndex: state.message.content.length, text: "", itemId, providerMetadata }, [id]: { contentIndex: state.message.content.length, text: "", providerMetadata },
}, },
} }
} }
const reduceReasoningDelta = (state: ResponseState, event: ReasoningDelta): ResponseState => { const reduceReasoningDelta = (state: ResponseState, event: ReasoningDelta): ResponseState => {
const started = ensureReasoning(state, event.id, event.itemId, event.providerMetadata) const started = ensureReasoning(state, event.id, event.providerMetadata)
const current = started.reasoningParts[event.id] const current = started.reasoningParts[event.id]
if (!current) return started if (!current) return started
const text = current.text + event.text const text = current.text + event.text
const itemId = event.itemId ?? current.itemId
const providerMetadata = event.providerMetadata ?? current.providerMetadata const providerMetadata = event.providerMetadata ?? current.providerMetadata
return { return {
...replaceContent(started, current.contentIndex, reasoningContent(text, itemId, providerMetadata)), ...replaceContent(started, current.contentIndex, reasoningContent(text, providerMetadata)),
reasoningParts: { ...started.reasoningParts, [event.id]: { ...current, text, itemId, providerMetadata } }, reasoningParts: { ...started.reasoningParts, [event.id]: { ...current, text, providerMetadata } },
} }
} }
@@ -508,10 +464,9 @@ const reduceReasoningEnd = (state: ResponseState, event: ReasoningEnd): Response
const current = state.reasoningParts[event.id] const current = state.reasoningParts[event.id]
if (!current) return state if (!current) return state
const providerMetadata = event.providerMetadata ?? current.providerMetadata const providerMetadata = event.providerMetadata ?? current.providerMetadata
const itemId = event.itemId ?? current.itemId
return { return {
...replaceContent(state, current.contentIndex, reasoningContent(current.text, itemId, providerMetadata)), ...replaceContent(state, current.contentIndex, reasoningContent(current.text, providerMetadata)),
reasoningParts: { ...state.reasoningParts, [event.id]: { ...current, itemId, providerMetadata } }, reasoningParts: { ...state.reasoningParts, [event.id]: { ...current, providerMetadata } },
} }
} }
@@ -519,7 +474,7 @@ const reduceToolInputStart = (state: ResponseState, event: ToolInputStart): Resp
...state, ...state,
toolInputs: { toolInputs: {
...state.toolInputs, ...state.toolInputs,
[event.id]: { name: event.name, text: "", itemId: event.itemId, providerMetadata: event.providerMetadata }, [event.id]: { name: event.name, text: "", providerMetadata: event.providerMetadata },
}, },
}) })
@@ -540,7 +495,6 @@ const reduceToolInputEnd = (state: ResponseState, event: ToolInputEnd): Response
[event.id]: { [event.id]: {
...current, ...current,
name: event.name, name: event.name,
itemId: event.itemId ?? current.itemId,
providerMetadata: event.providerMetadata ?? current.providerMetadata, providerMetadata: event.providerMetadata ?? current.providerMetadata,
}, },
}, },
@@ -550,7 +504,6 @@ const reduceToolInputEnd = (state: ResponseState, event: ToolInputEnd): Response
const toolCallContent = (event: ToolCall): ContentPart => const toolCallContent = (event: ToolCall): ContentPart =>
ToolCallPart.make({ ToolCallPart.make({
id: event.id, id: event.id,
...(event.itemId === undefined ? {} : { itemId: event.itemId }),
name: event.name, name: event.name,
input: event.input, input: event.input,
...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }), ...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }),
@@ -560,7 +513,6 @@ const toolCallContent = (event: ToolCall): ContentPart =>
const toolResultContent = (event: ToolResult): ContentPart => const toolResultContent = (event: ToolResult): ContentPart =>
ToolResultPart.make({ ToolResultPart.make({
id: event.id, id: event.id,
...(event.itemId === undefined ? {} : { itemId: event.itemId }),
name: event.name, name: event.name,
result: event.result, result: event.result,
...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }), ...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }),
@@ -576,13 +528,13 @@ const reduceResponseState = (state: ResponseState, event: LLMEvent): ResponseSta
const next = appendEvent(state, event) const next = appendEvent(state, event)
switch (event.type) { switch (event.type) {
case "text-start": case "text-start":
return ensureText(next, event.id, event.itemId, event.providerMetadata) return ensureText(next, event.id, event.providerMetadata)
case "text-delta": case "text-delta":
return reduceTextDelta(next, event) return reduceTextDelta(next, event)
case "text-end": case "text-end":
return reduceTextEnd(next, event) return reduceTextEnd(next, event)
case "reasoning-start": case "reasoning-start":
return ensureReasoning(next, event.id, event.itemId, event.providerMetadata) return ensureReasoning(next, event.id, event.providerMetadata)
case "reasoning-delta": case "reasoning-delta":
return reduceReasoningDelta(next, event) return reduceReasoningDelta(next, event)
case "reasoning-end": case "reasoning-end":
-3
View File
@@ -21,9 +21,6 @@ export type ProviderID = typeof ProviderID.Type
export const ResponseID = Schema.String export const ResponseID = Schema.String
export type ResponseID = Schema.Schema.Type<typeof ResponseID> export type ResponseID = Schema.Schema.Type<typeof ResponseID>
export const ResponseItemID = Schema.String
export type ResponseItemID = Schema.Schema.Type<typeof ResponseItemID>
export const ContentBlockID = Schema.String export const ContentBlockID = Schema.String
export type ContentBlockID = Schema.Schema.Type<typeof ContentBlockID> export type ContentBlockID = Schema.Schema.Type<typeof ContentBlockID>
+5 -7
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool" import { Tool } from "@opencode-ai/schema/tool"
import { JsonSchema, MessageRole, ProviderMetadata, ResponseItemID } from "./ids" import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, LanguageModelSchema, ProviderOptions } from "./options" import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, LanguageModelSchema, ProviderOptions } from "./options"
import { isRecord } from "../utils/record" import { isRecord } from "../utils/record"
@@ -25,7 +25,6 @@ export const SystemPart = Object.assign(systemPartSchema, {
export const TextPart = Schema.Struct({ export const TextPart = Schema.Struct({
type: Schema.Literal("text"), type: Schema.Literal("text"),
text: Schema.String, text: Schema.String,
itemId: Schema.optional(ResponseItemID),
cache: Schema.optional(CacheHint), cache: Schema.optional(CacheHint),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
@@ -122,7 +121,6 @@ export const ToolCallPart = Object.assign(
Schema.Struct({ Schema.Struct({
type: Schema.Literal("tool-call"), type: Schema.Literal("tool-call"),
id: Schema.String, id: Schema.String,
itemId: Schema.optional(ResponseItemID),
name: Schema.String, name: Schema.String,
input: Schema.Unknown, input: Schema.Unknown,
providerExecuted: Schema.optional(Schema.Boolean), providerExecuted: Schema.optional(Schema.Boolean),
@@ -140,7 +138,6 @@ export const ToolResultPart = Object.assign(
Schema.Struct({ Schema.Struct({
type: Schema.Literal("tool-result"), type: Schema.Literal("tool-result"),
id: Schema.String, id: Schema.String,
itemId: Schema.optional(ResponseItemID),
name: Schema.String, name: Schema.String,
result: ToolResultValue, result: ToolResultValue,
providerExecuted: Schema.optional(Schema.Boolean), providerExecuted: Schema.optional(Schema.Boolean),
@@ -157,7 +154,6 @@ export const ToolResultPart = Object.assign(
): ToolResultPart => ({ ): ToolResultPart => ({
type: "tool-result", type: "tool-result",
id: input.id, id: input.id,
...(input.itemId === undefined ? {} : { itemId: input.itemId }),
name: input.name, name: input.name,
result: ToolResultValue.make(input.result, input.resultType), result: ToolResultValue.make(input.result, input.resultType),
providerExecuted: input.providerExecuted, providerExecuted: input.providerExecuted,
@@ -172,7 +168,6 @@ export type ToolResultPart = Schema.Schema.Type<typeof ToolResultPart>
export const ReasoningPart = Schema.Struct({ export const ReasoningPart = Schema.Struct({
type: Schema.Literal("reasoning"), type: Schema.Literal("reasoning"),
text: Schema.String, text: Schema.String,
itemId: Schema.optional(ResponseItemID),
encrypted: Schema.optional(Schema.String), encrypted: Schema.optional(Schema.String),
cache: Schema.optional(CacheHint), cache: Schema.optional(CacheHint),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
@@ -186,7 +181,7 @@ export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, Tool
export type ContentPart = Schema.Schema.Type<typeof ContentPart> export type ContentPart = Schema.Schema.Type<typeof ContentPart>
export class Message extends Schema.Class<Message>("LLM.Message")({ export class Message extends Schema.Class<Message>("LLM.Message")({
id: Schema.optional(ResponseItemID), id: Schema.optional(Schema.String),
role: MessageRole, role: MessageRole,
content: Schema.Array(ContentPart), content: Schema.Array(ContentPart),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
@@ -277,6 +272,8 @@ export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
providerOptions: Schema.optional(ProviderOptions), providerOptions: Schema.optional(ProviderOptions),
http: Schema.optional(HttpOptions), http: Schema.optional(HttpOptions),
cache: Schema.optional(CachePolicy), cache: Schema.optional(CachePolicy),
// Stable cache affinity for protocols that support provider-managed prompt caching.
promptCacheKey: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {} }) {}
@@ -294,6 +291,7 @@ export namespace LLMRequest {
providerOptions: request.providerOptions, providerOptions: request.providerOptions,
http: request.http, http: request.http,
cache: request.cache, cache: request.cache,
promptCacheKey: request.promptCacheKey,
metadata: request.metadata, metadata: request.metadata,
}) })
+2 -2
View File
@@ -79,7 +79,7 @@ const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement,
id: call.id, id: call.id,
name: call.name, name: call.name,
result: settlement.result, result: settlement.result,
...(call.providerMetadata === undefined ? {} : { providerMetadata: call.providerMetadata }), providerMetadata: call.providerMetadata,
}), }),
] ]
: [ : [
@@ -88,7 +88,7 @@ const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement,
name: call.name, name: call.name,
result: settlement.result, result: settlement.result,
output: settlement.output, output: settlement.output,
...(call.providerMetadata === undefined ? {} : { providerMetadata: call.providerMetadata }), providerMetadata: call.providerMetadata,
}), }),
], ],
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -14
View File
@@ -8,6 +8,7 @@ import {
type ProviderMetadata, type ProviderMetadata,
type ToolCallPart, type ToolCallPart,
ToolResultPart, ToolResultPart,
type ToolResultValue,
type Usage, type Usage,
} from "../../src/schema" } from "../../src/schema"
import { type Tools, toDefinitions } from "../../src/tool" import { type Tools, toDefinitions } from "../../src/tool"
@@ -60,10 +61,9 @@ export const runTools = <T extends Tools>(options: RunOptions<T>) =>
...dispatched.map(([call, dispatched]) => ...dispatched.map(([call, dispatched]) =>
Message.tool({ Message.tool({
id: call.id, id: call.id,
itemId: dispatched.events.find(LLMEvent.is.toolResult)?.itemId,
name: call.name, name: call.name,
result: dispatched.result, result: dispatched.result,
providerMetadata: dispatched.events.find(LLMEvent.is.toolResult)?.providerMetadata, providerMetadata: call.providerMetadata,
}), }),
), ),
], ],
@@ -89,15 +89,9 @@ const stepState = (events: ReadonlyArray<LLMEvent>) => {
for (const event of events) { for (const event of events) {
if (event.type === "text-delta" || event.type === "reasoning-delta") { if (event.type === "text-delta" || event.type === "reasoning-delta") {
appendText(assistantContent, event.type === "text-delta" ? "text" : "reasoning", event.text, event.itemId) appendText(assistantContent, event.type === "text-delta" ? "text" : "reasoning", event.text)
} else if (event.type === "text-end" || event.type === "reasoning-end") { } else if (event.type === "text-end" || event.type === "reasoning-end") {
appendText( appendText(assistantContent, event.type === "text-end" ? "text" : "reasoning", "", event.providerMetadata)
assistantContent,
event.type === "text-end" ? "text" : "reasoning",
"",
event.itemId,
event.providerMetadata,
)
} else if (event.type === "tool-call") { } else if (event.type === "tool-call") {
assistantContent.push(event) assistantContent.push(event)
if (!event.providerExecuted) toolCalls.push(event) if (!event.providerExecuted) toolCalls.push(event)
@@ -105,7 +99,6 @@ const stepState = (events: ReadonlyArray<LLMEvent>) => {
assistantContent.push( assistantContent.push(
ToolResultPart.make({ ToolResultPart.make({
id: event.id, id: event.id,
itemId: event.itemId,
name: event.name, name: event.name,
result: event.result, result: event.result,
providerExecuted: true, providerExecuted: true,
@@ -125,7 +118,6 @@ const appendText = (
content: ContentPart[], content: ContentPart[],
type: "text" | "reasoning", type: "text" | "reasoning",
text: string, text: string,
itemId?: string,
providerMetadata?: ProviderMetadata, providerMetadata?: ProviderMetadata,
) => { ) => {
const last = content.at(-1) const last = content.at(-1)
@@ -133,12 +125,11 @@ const appendText = (
content[content.length - 1] = { content[content.length - 1] = {
...last, ...last,
text: `${last.text}${text}`, text: `${last.text}${text}`,
itemId: itemId ?? last.itemId,
providerMetadata: providerMetadata ?? last.providerMetadata, providerMetadata: providerMetadata ?? last.providerMetadata,
} }
return return
} }
content.push({ type, text, itemId, providerMetadata }) content.push({ type, text, providerMetadata })
} }
const addUsage = (left: Usage | undefined, right: Usage | undefined): Usage | undefined => { const addUsage = (left: Usage | undefined, right: Usage | undefined): Usage | undefined => {
@@ -3,11 +3,11 @@ import { CloudflareWorkersAI } from "../../src/providers"
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model") const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } }) LLM.request({ model, prompt: "Hello", promptCacheKey: "cache" })
LLM.request({ LLM.request({
model, model,
prompt: "Hello", prompt: "Hello",
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string. // @ts-expect-error Prompt cache keys must be strings.
providerOptions: { openai: { promptCacheKey: 1 } }, promptCacheKey: 1,
}) })
@@ -15,6 +15,8 @@ import {
} from "../../src" } from "../../src"
import * as Azure from "../../src/providers/azure" import * as Azure from "../../src/providers/azure"
import * as OpenAI from "../../src/providers/openai" import * as OpenAI from "../../src/providers/openai"
import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as XAI from "../../src/providers/xai"
import * as OpenAIChat from "../../src/protocols/openai-chat" import * as OpenAIChat from "../../src/protocols/openai-chat"
import { ProviderShared } from "../../src/protocols/shared" import { ProviderShared } from "../../src/protocols/shared"
import { Auth, LLMClient } from "../../src/route" import { Auth, LLMClient } from "../../src/route"
@@ -154,6 +156,47 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("maps the request prompt cache key", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenAICompatible.configure({
baseURL: "https://api.compatible.test/v1",
apiKey: "test",
}).model("compatible-model"),
prompt: "Hello",
promptCacheKey: "session_123",
}),
)
expect(prepared.body.prompt_cache_key).toBe("session_123")
}),
)
it.effect("maps the xAI Chat prompt cache key to conversation affinity", () =>
LLMClient.generate(
LLM.request({
model: XAI.configure({ apiKey: "test", baseURL: "https://api.x.ai/v1" }).chat("grok-4.5"),
prompt: "Hello",
promptCacheKey: "session_123",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(web.headers.get("x-grok-conv-id")).toBe("session_123")
const body = decodeJson(yield* Effect.promise(() => web.text()))
expect(ProviderShared.isRecord(body) ? body.prompt_cache_key : undefined).toBe("session_123")
return input.respond(sseEvents(deltaChunk({}, "stop")), {
headers: { "content-type": "text/event-stream" },
})
}),
),
),
),
)
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () => it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -45,7 +45,7 @@ describe("Open Responses-compatible route", () => {
}, },
}, },
}) })
expect(prepared.body).toMatchObject({ expect(prepared.body).toEqual({
model: "example-model", model: "example-model",
input: [ input: [
{ role: "system", content: "You are concise." }, { role: "system", content: "You are concise." },
@@ -53,8 +53,6 @@ describe("Open Responses-compatible route", () => {
], ],
stream: true, stream: true,
}) })
expect(prepared.body.input[0]).not.toHaveProperty("id")
expect(prepared.body.input[1]).not.toHaveProperty("id")
}), }),
) )
@@ -20,7 +20,7 @@ const cacheRequest = LLM.request({
system: LARGE_CACHEABLE_SYSTEM, system: LARGE_CACHEABLE_SYSTEM,
prompt: "Say hi.", prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 }, generation: { maxTokens: 16, temperature: 0 },
providerOptions: { openai: { promptCacheKey: "recorded-cache-test" } }, promptCacheKey: "recorded-cache-test",
}) })
const recorded = recordedTests({ const recorded = recordedTests({
@@ -69,9 +69,6 @@ describe("OpenAI Responses route", () => {
stream: true, stream: true,
max_output_tokens: 20, max_output_tokens: 20,
temperature: 0, temperature: 0,
tool_choice: undefined,
tools: undefined,
top_p: undefined,
}) })
}), }),
) )
@@ -332,7 +329,7 @@ describe("OpenAI Responses route", () => {
yield* LLMClient.generate( yield* LLMClient.generate(
LLMRequest.update(request, { LLMRequest.update(request, {
model: Azure.configure({ model: Azure.configure({
baseURL: "https://opencode-test.openai.azure.com/openai/", baseURL: "https://opencode-test.openai.azure.com/openai/v1/",
apiKey: "azure-key", apiKey: "azure-key",
headers: { authorization: "Bearer stale" }, headers: { authorization: "Bearer stale" },
}).responses("gpt-4.1-mini"), }).responses("gpt-4.1-mini"),
@@ -413,7 +410,7 @@ describe("OpenAI Responses route", () => {
}), }),
) )
expect(prepared.body).toMatchObject({ expect(prepared.body).toEqual({
model: "gpt-4.1-mini", model: "gpt-4.1-mini",
input: [ input: [
{ role: "user", content: [{ type: "input_text", text: "What is the weather?" }] }, { role: "user", content: [{ type: "input_text", text: "What is the weather?" }] },
@@ -428,65 +425,6 @@ describe("OpenAI Responses route", () => {
tools: undefined, tools: undefined,
top_p: undefined, top_p: undefined,
}) })
const call = prepared.body.input.find((item) => "type" in item && item.type === "function_call")
const output = prepared.body.input.find((item) => "type" in item && item.type === "function_call_output")
expect(call?.id).toBeUndefined()
expect(output?.id).toBeUndefined()
}),
)
it.effect("does not generate response item ids for client-created history", () =>
Effect.sync(() => {
const canonical = LLM.request({
model,
messages: [
Message.assistant([
{ type: "text", text: "Working." },
{ type: "reasoning", text: "Thinking." },
ToolCallPart.make({ id: "call_1", name: "lookup", input: {} }),
]),
Message.tool({ id: "call_1", name: "lookup", result: "done" }),
],
})
expect(canonical.messages[0]?.id).toBeUndefined()
expect(canonical.messages[1]?.id).toBeUndefined()
expect(canonical.messages[0]?.content.every((part) => part.type === "media" || part.itemId === undefined)).toBe(
true,
)
expect(canonical.messages[1]?.content[0]).not.toHaveProperty("itemId")
}),
)
it.effect("replays provider function call item ids without assigning output ids", () =>
Effect.gen(function* () {
const canonical = LLM.request({
model,
messages: [
Message.assistant([
{ type: "text", text: "Calling.", itemId: "plain-text" },
ToolCallPart.make({
id: "call_1",
name: "lookup",
input: {},
providerMetadata: { openai: { itemId: "plain-call" } },
}),
]),
Message.tool({ id: "call_1", itemId: "plain-output", name: "lookup", result: "done" }),
],
})
const prepared = yield* compileRequest(canonical)
expect(canonical.messages[0]?.content.map((part) => (part.type === "media" ? undefined : part.itemId))).toEqual([
"plain-text",
undefined,
])
expect(canonical.messages[1]?.content[0]).toMatchObject({ itemId: "plain-output" })
expect(prepared.body.input).toEqual([
{ role: "assistant", id: "plain-text", content: [{ type: "output_text", text: "Calling." }] },
{ type: "function_call", id: "plain-call", call_id: "call_1", name: "lookup", arguments: "{}" },
{ type: "function_call_output", call_id: "call_1", output: '"done"' },
])
}), }),
) )
@@ -744,9 +682,9 @@ describe("OpenAI Responses route", () => {
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "think", prompt: "think",
promptCacheKey: "session_123",
providerOptions: { providerOptions: {
openai: { openai: {
promptCacheKey: "session_123",
reasoningEffort: "high", reasoningEffort: "high",
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: ["reasoning.encrypted_content"],
@@ -865,17 +803,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* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
LLM.request({ LLM.request({
model: OpenAI.configure({ model: OpenAI.configure({
baseURL: "https://api.openai.test/v1/", baseURL: "https://api.openai.test/v1/",
apiKey: "test", apiKey: "test",
providerOptions: { openai: { promptCacheKey: "model_cache" } },
}).model("gpt-4.1-mini"), }).model("gpt-4.1-mini"),
prompt: "no cache", prompt: "no cache",
providerOptions: { openai: { promptCacheKey: "request_cache" } }, promptCacheKey: "request_cache",
}), }),
) )
@@ -926,21 +863,9 @@ describe("OpenAI Responses route", () => {
expect(response.text).toBe("Hello!") expect(response.text).toBe("Hello!")
expect(response.events).toEqual([ expect(response.events).toEqual([
{ type: "step-start", index: 0 }, { type: "step-start", index: 0 },
{ type: "text-start", id: "msg_1", itemId: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } }, { type: "text-start", id: "msg_1" },
{ { type: "text-delta", id: "msg_1", text: "Hello" },
type: "text-delta", { type: "text-delta", id: "msg_1", text: "!" },
id: "msg_1",
itemId: "msg_1",
text: "Hello",
providerMetadata: { openai: { itemId: "msg_1" } },
},
{
type: "text-delta",
id: "msg_1",
itemId: "msg_1",
text: "!",
providerMetadata: { openai: { itemId: "msg_1" } },
},
{ type: "text-end", id: "msg_1" }, { type: "text-end", id: "msg_1" },
{ {
type: "step-finish", type: "step-finish",
@@ -997,20 +922,17 @@ describe("OpenAI Responses route", () => {
{ {
type: "text", type: "text",
text: "Checking.", text: "Checking.",
itemId: "msg_commentary", providerMetadata: { openai: { phase: "commentary" } },
providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } },
}, },
{ {
type: "text", type: "text",
text: "Finished.", text: "Finished.",
itemId: "msg_final", providerMetadata: { openai: { phase: "final_answer" } },
providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } },
}, },
{ {
type: "text", type: "text",
text: "Unclassified.", text: "Unclassified.",
itemId: "msg_null", providerMetadata: { openai: { phase: null } },
providerMetadata: { openai: { itemId: "msg_null", phase: null } },
}, },
]) ])
@@ -1018,19 +940,16 @@ describe("OpenAI Responses route", () => {
expect(prepared.body.input).toEqual([ expect(prepared.body.input).toEqual([
{ {
role: "assistant", role: "assistant",
id: "msg_commentary",
content: [{ type: "output_text", text: "Checking." }], content: [{ type: "output_text", text: "Checking." }],
phase: "commentary", phase: "commentary",
}, },
{ {
role: "assistant", role: "assistant",
id: "msg_final",
content: [{ type: "output_text", text: "Finished." }], content: [{ type: "output_text", text: "Finished." }],
phase: "final_answer", phase: "final_answer",
}, },
{ {
role: "assistant", role: "assistant",
id: "msg_null",
content: [{ type: "output_text", text: "Unclassified." }], content: [{ type: "output_text", text: "Unclassified." }],
phase: null, phase: null,
}, },
@@ -1123,24 +1042,12 @@ describe("OpenAI Responses route", () => {
) )
expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([ expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([
{ type: "text-start", id: "msg_1", itemId: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } }, { type: "text-start", id: "msg_1" },
{ { type: "text-delta", id: "msg_1", text: "First" },
type: "text-delta", { type: "text-end", id: "msg_1" },
id: "msg_1", { type: "text-start", id: "msg_2" },
itemId: "msg_1", { type: "text-delta", id: "msg_2", text: "Second" },
text: "First", { type: "text-end", id: "msg_2" },
providerMetadata: { openai: { itemId: "msg_1" } },
},
{ type: "text-end", id: "msg_1", itemId: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
{ type: "text-start", id: "msg_2", itemId: "msg_2", providerMetadata: { openai: { itemId: "msg_2" } } },
{
type: "text-delta",
id: "msg_2",
itemId: "msg_2",
text: "Second",
providerMetadata: { openai: { itemId: "msg_2" } },
},
{ type: "text-end", id: "msg_2", itemId: "msg_2", providerMetadata: { openai: { itemId: "msg_2" } } },
]) ])
}), }),
) )
@@ -1160,15 +1067,9 @@ describe("OpenAI Responses route", () => {
expect(response.text).toBe("Hello") expect(response.text).toBe("Hello")
expect(response.events).toMatchObject([ expect(response.events).toMatchObject([
{ type: "step-start", index: 0 }, { type: "step-start", index: 0 },
{ type: "reasoning-start", id: "rs_1", itemId: "rs_1", providerMetadata: { openai: { itemId: "rs_1" } } }, { type: "reasoning-start", id: "rs_1" },
{ { type: "reasoning-delta", id: "rs_1", text: "thinking" },
type: "reasoning-delta", { type: "text-start", id: "msg_1" },
id: "rs_1",
itemId: "rs_1",
text: "thinking",
providerMetadata: { openai: { itemId: "rs_1" } },
},
{ type: "text-start", id: "msg_1", itemId: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
{ type: "text-delta", id: "msg_1", text: "Hello" }, { type: "text-delta", id: "msg_1", text: "Hello" },
{ type: "reasoning-end", id: "rs_1" }, { type: "reasoning-end", id: "rs_1" },
{ type: "text-end", id: "msg_1" }, { type: "text-end", id: "msg_1" },
@@ -1177,8 +1078,8 @@ describe("OpenAI Responses route", () => {
]) ])
expect(response.events.filter((event) => event.type === "finish")).toHaveLength(1) expect(response.events.filter((event) => event.type === "finish")).toHaveLength(1)
expect(response.message.content).toEqual([ expect(response.message.content).toEqual([
{ type: "reasoning", text: "thinking", itemId: "rs_1", providerMetadata: { openai: { itemId: "rs_1" } } }, { type: "reasoning", text: "thinking" },
{ type: "text", text: "Hello", itemId: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } }, { type: "text", text: "Hello" },
]) ])
}), }),
) )
@@ -1209,7 +1110,6 @@ describe("OpenAI Responses route", () => {
expect.objectContaining({ expect.objectContaining({
type: "reasoning-end", type: "reasoning-end",
id: "rs_1", id: "rs_1",
itemId: "rs_1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
}), }),
) )
@@ -1250,34 +1150,19 @@ describe("OpenAI Responses route", () => {
{ {
type: "reasoning-start", type: "reasoning-start",
id: "rs_1:0", id: "rs_1:0",
itemId: "rs_1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } }, providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
}, },
{ { type: "reasoning-delta", id: "rs_1:0", text: "First" },
type: "reasoning-delta", { type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
id: "rs_1:0",
itemId: "rs_1",
text: "First",
providerMetadata: { openai: { itemId: "rs_1" } },
},
{ type: "reasoning-end", id: "rs_1:0", itemId: "rs_1", providerMetadata: { openai: { itemId: "rs_1" } } },
{ {
type: "reasoning-start", type: "reasoning-start",
id: "rs_1:1", id: "rs_1:1",
itemId: "rs_1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } }, providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
}, },
{ { type: "reasoning-delta", id: "rs_1:1", text: "Second" },
type: "reasoning-delta",
id: "rs_1:1",
itemId: "rs_1",
text: "Second",
providerMetadata: { openai: { itemId: "rs_1" } },
},
{ {
type: "reasoning-end", type: "reasoning-end",
id: "rs_1:1", id: "rs_1:1",
itemId: "rs_1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
}, },
{ type: "step-finish", index: 0, reason: { normalized: "stop", raw: undefined } }, { type: "step-finish", index: 0, reason: { normalized: "stop", raw: undefined } },
@@ -1315,8 +1200,8 @@ describe("OpenAI Responses route", () => {
) )
expect(response.events.filter((event) => event.type === "reasoning-end")).toEqual([ expect(response.events.filter((event) => event.type === "reasoning-end")).toEqual([
{ type: "reasoning-end", id: "rs_1:0", itemId: "rs_1", providerMetadata: { openai: { itemId: "rs_1" } } }, { type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
{ type: "reasoning-end", id: "rs_1:1", itemId: "rs_1", providerMetadata: { openai: { itemId: "rs_1" } } }, { type: "reasoning-end", id: "rs_1:1", providerMetadata: { openai: { itemId: "rs_1" } } },
]) ])
}), }),
) )
@@ -1364,7 +1249,7 @@ describe("OpenAI Responses route", () => {
{ role: "user", content: [{ type: "input_text", text: "Summarize it." }] }, { role: "user", content: [{ type: "input_text", text: "Summarize it." }] },
], ],
}) })
expect(body.input[1]).toHaveProperty("id", "rs_1") expect(body.input[1]).not.toHaveProperty("id")
return input.respond( return input.respond(
sseEvents( sseEvents(
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Parser now round-trips reasoning." }, { type: "response.output_text.delta", item_id: "msg_1", delta: "Parser now round-trips reasoning." },
@@ -1411,7 +1296,6 @@ describe("OpenAI Responses route", () => {
{ role: "assistant", content: [{ type: "output_text", text: "Before." }] }, { role: "assistant", content: [{ type: "output_text", text: "Before." }] },
{ {
type: "reasoning", type: "reasoning",
id: "rs_1",
encrypted_content: "encrypted-state", encrypted_content: "encrypted-state",
summary: [{ type: "summary_text", text: "Checked order." }], summary: [{ type: "summary_text", text: "Checked order." }],
}, },
@@ -1420,7 +1304,7 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("replays complete stored reasoning items with their id", () => it.effect("references stored reasoning items by id", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
LLM.request({ LLM.request({
@@ -1438,14 +1322,7 @@ describe("OpenAI Responses route", () => {
}), }),
) )
expect(prepared.body.input).toEqual([ expect(prepared.body.input).toEqual([{ type: "item_reference", id: "rs_1" }])
{
type: "reasoning",
id: "rs_1",
summary: [{ type: "summary_text", text: "Checked the previous diff." }],
encrypted_content: undefined,
},
])
}), }),
) )
@@ -1554,7 +1431,6 @@ describe("OpenAI Responses route", () => {
expect(prepared.body.input).toEqual([ expect(prepared.body.input).toEqual([
{ {
type: "reasoning", type: "reasoning",
id: "rs_1",
encrypted_content: "encrypted-state", encrypted_content: "encrypted-state",
summary: [ summary: [
{ type: "summary_text", text: "First" }, { type: "summary_text", text: "First" },
@@ -1634,7 +1510,6 @@ describe("OpenAI Responses route", () => {
outputTokens: 1, outputTokens: 1,
nonCachedInputTokens: 5, nonCachedInputTokens: 5,
cacheReadInputTokens: undefined, cacheReadInputTokens: undefined,
cacheWriteInputTokens: undefined,
reasoningTokens: undefined, reasoningTokens: undefined,
totalTokens: 6, totalTokens: 6,
providerMetadata: { openai: { input_tokens: 5, output_tokens: 1 } }, providerMetadata: { openai: { input_tokens: 5, output_tokens: 1 } },
@@ -1645,35 +1520,30 @@ describe("OpenAI Responses route", () => {
{ {
type: "tool-input-start", type: "tool-input-start",
id: "call_1", id: "call_1",
itemId: "item_1",
name: "lookup", name: "lookup",
providerMetadata: { openai: { itemId: "item_1" } }, providerMetadata: { openai: { itemId: "item_1" } },
}, },
{ {
type: "tool-input-delta", type: "tool-input-delta",
id: "call_1", id: "call_1",
itemId: "item_1",
name: "lookup", name: "lookup",
text: '{"query"', text: '{"query"',
}, },
{ {
type: "tool-input-delta", type: "tool-input-delta",
id: "call_1", id: "call_1",
itemId: "item_1",
name: "lookup", name: "lookup",
text: ':"weather"}', text: ':"weather"}',
}, },
{ {
type: "tool-input-end", type: "tool-input-end",
id: "call_1", id: "call_1",
itemId: "item_1",
name: "lookup", name: "lookup",
providerMetadata: { openai: { itemId: "item_1" } }, providerMetadata: { openai: { itemId: "item_1" } },
}, },
{ {
type: "tool-call", type: "tool-call",
id: "call_1", id: "call_1",
itemId: "item_1",
name: "lookup", name: "lookup",
input: { query: "weather" }, input: { query: "weather" },
providerExecuted: undefined, providerExecuted: undefined,
@@ -1693,17 +1563,6 @@ describe("OpenAI Responses route", () => {
usage, usage,
}, },
]) ])
expect(response.message.content).toEqual([
{
type: "tool-call",
id: "call_1",
itemId: "item_1",
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: { openai: { itemId: "item_1" } },
},
])
}), }),
) )
@@ -1736,7 +1595,6 @@ describe("OpenAI Responses route", () => {
expect(response.events.find(LLMEvent.is.toolInputError)).toEqual({ expect(response.events.find(LLMEvent.is.toolInputError)).toEqual({
type: "tool-input-error", type: "tool-input-error",
id: "call_1", id: "call_1",
itemId: "item_1",
name: "lookup", name: "lookup",
raw: '{"query":"partial', raw: '{"query":"partial',
}) })
@@ -1793,7 +1651,6 @@ describe("OpenAI Responses route", () => {
{ {
type: "tool-call", type: "tool-call",
id: "ws_1", id: "ws_1",
itemId: "ws_1",
name: "web_search", name: "web_search",
input: { type: "search", query: "effect 4" }, input: { type: "search", query: "effect 4" },
providerExecuted: true, providerExecuted: true,
@@ -1802,35 +1659,11 @@ describe("OpenAI Responses route", () => {
{ {
type: "tool-result", type: "tool-result",
id: "ws_1", id: "ws_1",
itemId: "ws_1",
name: "web_search", name: "web_search",
result: { type: "json", value: item }, result: { type: "json", value: item },
providerExecuted: true, providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1", item } },
output: undefined,
},
])
expect(response.message.content).toEqual([
{
type: "tool-call",
id: "ws_1",
itemId: "ws_1",
name: "web_search",
input: { type: "search", query: "effect 4" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } }, providerMetadata: { openai: { itemId: "ws_1" } },
}, },
{
type: "tool-result",
id: "ws_1",
itemId: "ws_1",
name: "web_search",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1", item } },
metadata: undefined,
cache: undefined,
},
]) ])
}), }),
) )
@@ -1908,7 +1741,6 @@ describe("OpenAI Responses route", () => {
expect(toolCall).toEqual({ expect(toolCall).toEqual({
type: "tool-call", type: "tool-call",
id: "ci_1", id: "ci_1",
itemId: "ci_1",
name: "code_interpreter", name: "code_interpreter",
input: { code: "print(1+1)", container_id: "cnt_xyz" }, input: { code: "print(1+1)", container_id: "cnt_xyz" },
providerExecuted: true, providerExecuted: true,
@@ -1918,12 +1750,10 @@ describe("OpenAI Responses route", () => {
expect(toolResult).toEqual({ expect(toolResult).toEqual({
type: "tool-result", type: "tool-result",
id: "ci_1", id: "ci_1",
itemId: "ci_1",
name: "code_interpreter", name: "code_interpreter",
result: { type: "json", value: item }, result: { type: "json", value: item },
providerExecuted: true, providerExecuted: true,
providerMetadata: { openai: { itemId: "ci_1", item } }, providerMetadata: { openai: { itemId: "ci_1" } },
output: undefined,
}) })
}), }),
) )
+1 -1
View File
@@ -162,7 +162,6 @@ describe("OpenRouter", () => {
openrouter: { openrouter: {
usage: true, usage: true,
reasoning: { effort: "high" }, reasoning: { effort: "high" },
promptCacheKey: "session_123",
models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"], models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"],
provider: { order: ["anthropic", "google"], require_parameters: true }, provider: { order: ["anthropic", "google"], require_parameters: true },
plugins: [{ id: "response-healing" }], plugins: [{ id: "response-healing" }],
@@ -174,6 +173,7 @@ describe("OpenRouter", () => {
}, },
}).model("anthropic/claude-3.7-sonnet:thinking"), }).model("anthropic/claude-3.7-sonnet:thinking"),
prompt: "Think briefly.", prompt: "Think briefly.",
promptCacheKey: "session_123",
}), }),
) )
-37
View File
@@ -49,43 +49,6 @@ describe("LLMResponse reducer", () => {
expect(state.message.content).toEqual([{ type: "text", text: "partial" }]) expect(state.message.content).toEqual([{ type: "text", text: "partial" }])
}) })
test("assembles response item identity and provider metadata", () => {
const response = LLMResponse.fromEvents([
LLMEvent.textStart({ id: "text-block", itemId: "msg_existing" }),
LLMEvent.textDelta({
id: "text-block",
itemId: "msg_existing",
text: "Answer",
providerMetadata: { openai: { itemId: "msg_existing" } },
}),
LLMEvent.textEnd({ id: "text-block", itemId: "msg_existing" }),
LLMEvent.reasoningStart({ id: "reasoning-block", itemId: "rs_existing" }),
LLMEvent.reasoningDelta({
id: "reasoning-block",
itemId: "rs_existing",
text: "Thought",
providerMetadata: { openai: { itemId: "rs_existing" } },
}),
LLMEvent.reasoningEnd({ id: "reasoning-block", itemId: "rs_existing" }),
LLMEvent.finish({ reason: { normalized: "stop" } }),
])
expect(response?.message.content).toEqual([
{
type: "text",
text: "Answer",
itemId: "msg_existing",
providerMetadata: { openai: { itemId: "msg_existing" } },
},
{
type: "reasoning",
text: "Thought",
itemId: "rs_existing",
providerMetadata: { openai: { itemId: "rs_existing" } },
},
])
})
test("does not complete ended content without a terminal finish", () => { test("does not complete ended content without a terminal finish", () => {
const state = reduce([ const state = reduce([
LLMEvent.textStart({ id: "t1" }), LLMEvent.textStart({ id: "t1" }),
+5 -29
View File
@@ -172,7 +172,7 @@ describe("LLMClient tools", () => {
expect(calls).toEqual([{ id: "call_projected", parameters: { prefix: "count" }, output: { count: "2" } }]) expect(calls).toEqual([{ id: "call_projected", parameters: { prefix: "count" }, output: { count: "2" } }])
expect(dispatched.result).toEqual({ type: "text", value: "count:2" }) expect(dispatched.result).toEqual({ type: "text", value: "count:2" })
expect(dispatched.output).toEqual({ structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] }) expect(dispatched.output).toEqual({ structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] })
expect(dispatched.events).toMatchObject([ expect(dispatched.events).toEqual([
LLMEvent.toolResult({ LLMEvent.toolResult({
id: "call_projected", id: "call_projected",
name: "projected", name: "projected",
@@ -180,7 +180,6 @@ describe("LLMClient tools", () => {
output: { structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] }, output: { structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] },
}), }),
]) ])
expect(dispatched.events[0]?.itemId).toBeUndefined()
}), }),
) )
@@ -198,7 +197,7 @@ describe("LLMClient tools", () => {
LLMEvent.toolCall({ id: "call_1", name: "tool", input: {}, providerMetadata }), LLMEvent.toolCall({ id: "call_1", name: "tool", input: {}, providerMetadata }),
) )
expect(dispatched.events).toMatchObject([ expect(dispatched.events).toEqual([
LLMEvent.toolResult({ LLMEvent.toolResult({
id: "call_1", id: "call_1",
name: "tool", name: "tool",
@@ -207,13 +206,12 @@ describe("LLMClient tools", () => {
providerMetadata, providerMetadata,
}), }),
]) ])
expect(dispatched.events[0]?.itemId).toBeUndefined()
const failed = yield* ToolRuntime.dispatch( const failed = yield* ToolRuntime.dispatch(
{}, {},
LLMEvent.toolCall({ id: "call_2", itemId: "fc_failed", name: "missing", input: {}, providerMetadata }), LLMEvent.toolCall({ id: "call_2", name: "missing", input: {}, providerMetadata }),
) )
expect(failed.events).toMatchObject([ expect(failed.events).toEqual([
LLMEvent.toolError({ LLMEvent.toolError({
id: "call_2", id: "call_2",
name: "missing", name: "missing",
@@ -227,27 +225,6 @@ describe("LLMClient tools", () => {
providerMetadata, providerMetadata,
}), }),
]) ])
const errorItemID = failed.events.find(LLMEvent.is.toolError)?.itemId
const resultItemID = failed.events.find(LLMEvent.is.toolResult)?.itemId
expect(errorItemID).toBeUndefined()
expect(resultItemID).toBeUndefined()
}),
)
it.effect("does not derive a function output item id from the function call item id", () =>
Effect.gen(function* () {
const tool = Tool.make({
description: "Return text.",
parameters: Schema.Struct({}),
success: Schema.String,
execute: () => Effect.succeed("hello"),
})
const dispatched = yield* ToolRuntime.dispatch(
{ tool },
LLMEvent.toolCall({ id: "call_1", itemId: "fc_existing", name: "tool", input: {} }),
)
expect(dispatched.events.find(LLMEvent.is.toolResult)?.itemId).toBeUndefined()
}), }),
) )
@@ -460,7 +437,7 @@ describe("LLMClient tools", () => {
) )
expect(dispatched.result).toEqual(callerOwned) expect(dispatched.result).toEqual(callerOwned)
expect(dispatched.events).toMatchObject([ expect(dispatched.events).toEqual([
LLMEvent.toolResult({ LLMEvent.toolResult({
id: "call_1", id: "call_1",
name: "eventful", name: "eventful",
@@ -468,7 +445,6 @@ describe("LLMClient tools", () => {
output: { structured: { ok: true }, content: [] }, output: { structured: { ok: true }, content: [] },
}), }),
]) ])
expect(dispatched.events[0]?.itemId).toBeUndefined()
}), }),
) )
+2 -1
View File
@@ -22,5 +22,6 @@
} }
}, },
"include": ["src", "package.json"], "include": ["src", "package.json"],
"exclude": ["dist", "ts-dist"] "exclude": ["dist", "ts-dist"],
"references": [{ "path": "../core" }]
} }
+1 -1
View File
@@ -11,7 +11,7 @@
"fix-node-pty": "bun run script/fix-node-pty.ts", "fix-node-pty": "bun run script/fix-node-pty.ts",
"benchmark:location": "bun run script/benchmark-location.ts", "benchmark:location": "bun run script/benchmark-location.ts",
"test": "bun test --only-failures", "test": "bun test --only-failures",
"typecheck": "tsgo --noEmit" "typecheck": "tsgo -b tsconfig.json tsconfig.tests.json"
}, },
"bin": { "bin": {
"opencode": "./bin/opencode" "opencode": "./bin/opencode"
+10 -6
View File
@@ -132,14 +132,16 @@ function renderMigration(name: string, sql: string) {
return `import { Effect } from "effect" return `import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: ${JSON.stringify(name)}, id: ${JSON.stringify(name)},
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
${renderStatements(sql)} ${renderStatements(sql)}
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
` `
} }
@@ -147,13 +149,15 @@ function renderSchema(sql: string) {
return `import { Effect } from "effect" return `import { Effect } from "effect"
import type { DatabaseMigration } from "./migration" import type { DatabaseMigration } from "./migration"
export default { const schema: Omit<DatabaseMigration.Migration, "id"> = {
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
${renderStatements(sql)} ${renderStatements(sql)}
}) })
}, },
} satisfies Omit<DatabaseMigration.Migration, "id"> }
export default schema
` `
} }
@@ -191,10 +195,10 @@ async function formatTypescript(input: string) {
function renderRegistry(names: string[]) { function renderRegistry(names: string[]) {
return `import type { DatabaseMigration } from "./migration" return `import type { DatabaseMigration } from "./migration"
export const migrations = ( export const migrations: DatabaseMigration.Migration[] = (
await Promise.all([ await Promise.all([
${names.map((name) => ` import("./migration/${name}"),`).join("\n")} ${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
]) ])
).map((module) => module.default) satisfies DatabaseMigration.Migration[] ).map((module) => module.default)
` `
} }
+1 -1
View File
@@ -263,6 +263,7 @@ function mapOpenRouterOptions(settings: Readonly<Record<string, unknown>>) {
"extraBody", "extraBody",
"fetch", "fetch",
"headers", "headers",
"promptCacheKey",
"timeout", "timeout",
].includes(key), ].includes(key),
), ),
@@ -279,7 +280,6 @@ function mapXAIOptions(settings: Readonly<Record<string, unknown>>) {
const options = { const options = {
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}), ...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
...(typeof settings.store === "boolean" ? { store: settings.store } : {}), ...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
} }
if (Object.keys(options).length === 0) return {} if (Object.keys(options).length === 0) return {}
return { providerOptions: { xai: options } } return { providerOptions: { xai: options } }
+1 -1
View File
@@ -126,7 +126,7 @@ ${render(current)}`
const key = Instructions.Key.make("core/codemode") const key = Instructions.Key.make("core/codemode")
const codec = Schema.toCodecJson(CodeModeCatalog.Summary) const codec = Schema.toCodecJson(CodeModeCatalog.Summary)
export const make = (entries?: ReadonlyArray<CodeModeCatalog.Entry>): Instructions.Instructions => { export const make = (entries?: ReadonlyArray<CodeModeCatalog.Entry>): Instructions.List => {
const catalog = entries === undefined ? Instructions.removed : CodeModeCatalog.summarize(entries) const catalog = entries === undefined ? Instructions.removed : CodeModeCatalog.summarize(entries)
return Instructions.make({ return Instructions.make({
key, key,
+2 -2
View File
@@ -1,6 +1,6 @@
import type { DatabaseMigration } from "./migration" import type { DatabaseMigration } from "./migration"
export const migrations = ( export const migrations: DatabaseMigration.Migration[] = (
await Promise.all([ await Promise.all([
import("./migration/20260127222353_familiar_lady_ursula"), import("./migration/20260127222353_familiar_lady_ursula"),
import("./migration/20260211171708_add_project_commands"), import("./migration/20260211171708_add_project_commands"),
@@ -43,4 +43,4 @@ export const migrations = (
import("./migration/20260804233008_loose_psylocke"), import("./migration/20260804233008_loose_psylocke"),
import("./migration/20260805200742_import_legacy_credentials"), import("./migration/20260805200742_import_legacy_credentials"),
]) ])
).map((module) => module.default) satisfies DatabaseMigration.Migration[] ).map((module) => module.default)
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260127222353_familiar_lady_ursula", id: "20260127222353_familiar_lady_ursula",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -104,4 +104,6 @@ export default {
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,11 +1,13 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260211171708_add_project_commands", id: "20260211171708_add_project_commands",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`) yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260213144116_wakeful_the_professor", id: "20260213144116_wakeful_the_professor",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -20,4 +20,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260225215848_workspace", id: "20260225215848_workspace",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -16,4 +16,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260227213759_add_session_workspace_id", id: "20260227213759_add_session_workspace_id",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -9,4 +9,6 @@ export default {
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260228203230_blue_harpoon", id: "20260228203230_blue_harpoon",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -27,4 +27,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260303231226_add_workspace_fields", id: "20260303231226_add_workspace_fields",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -12,4 +12,6 @@ export default {
yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`) yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260309230000_move_org_to_state", id: "20260309230000_move_org_to_state",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -12,4 +12,6 @@ export default {
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`) yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260312043431_session_message_cursor", id: "20260312043431_session_message_cursor",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -13,4 +13,6 @@ export default {
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260323234822_events", id: "20260323234822_events",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -23,4 +23,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260410174513_workspace-name", id: "20260410174513_workspace-name",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -26,4 +26,6 @@ export default {
yield* tx.run(`PRAGMA foreign_keys=ON;`) yield* tx.run(`PRAGMA foreign_keys=ON;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260413175956_chief_energizer", id: "20260413175956_chief_energizer",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -21,4 +21,6 @@ export default {
yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`) yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260423070820_add_icon_url_override", id: "20260423070820_add_icon_url_override",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -11,4 +11,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260427172553_slow_nightmare", id: "20260427172553_slow_nightmare",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -27,4 +27,6 @@ export default {
yield* tx.run(`DROP TABLE \`session_entry\`;`) yield* tx.run(`DROP TABLE \`session_entry\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,11 +1,13 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260428004200_add_session_path", id: "20260428004200_add_session_path",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`) yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260501142318_next_venus", id: "20260501142318_next_venus",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -9,4 +9,6 @@ export default {
yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`) yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,11 +1,13 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260504145000_add_sync_owner", id: "20260504145000_add_sync_owner",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`) yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,11 +1,13 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260507164347_add_workspace_time", id: "20260507164347_add_workspace_time",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`) yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260510033149_session_usage", id: "20260510033149_session_usage",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -53,4 +53,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260511000411_data_migration_state", id: "20260511000411_data_migration_state",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -13,4 +13,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260511173437_session-metadata", id: "20260511173437_session-metadata",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -13,4 +13,6 @@ export default {
yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`) yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260601010001_normalize_storage_paths", id: "20260601010001_normalize_storage_paths",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -19,4 +19,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,11 +1,13 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260601202201_amazing_prowler", id: "20260601202201_amazing_prowler",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`DROP TABLE \`permission\`;`) yield* tx.run(`DROP TABLE \`permission\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260602002951_lowly_union_jack", id: "20260602002951_lowly_union_jack",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -21,4 +21,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260602182828_add_project_directories", id: "20260602182828_add_project_directories",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -17,4 +17,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260603001617_session_message_projection_indexes", id: "20260603001617_session_message_projection_indexes",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -16,4 +16,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260603040000_session_message_projection_order", id: "20260603040000_session_message_projection_order",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -16,4 +16,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260603141458_session_input_inbox", id: "20260603141458_session_input_inbox",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -22,4 +22,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260603160727_jittery_ezekiel_stane", id: "20260603160727_jittery_ezekiel_stane",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -17,4 +17,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260604172448_event_sourced_session_input", id: "20260604172448_event_sourced_session_input",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -44,4 +44,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260605003541_add_session_context_snapshot", id: "20260605003541_add_session_context_snapshot",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -18,4 +18,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,11 +1,13 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260605042240_add_context_epoch_agent", id: "20260605042240_add_context_epoch_agent",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`session_context_epoch\` ADD \`agent\` text DEFAULT 'build' NOT NULL;`) yield* tx.run(`ALTER TABLE \`session_context_epoch\` ADD \`agent\` text DEFAULT 'build' NOT NULL;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260611035744_credential", id: "20260611035744_credential",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -22,4 +22,6 @@ export default {
) )
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260611192811_lush_chimera", id: "20260611192811_lush_chimera",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -22,4 +22,6 @@ export default {
`) `)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260612174303_project_dir_strategy", id: "20260612174303_project_dir_strategy",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -26,4 +26,6 @@ export default {
yield* tx.run(`PRAGMA foreign_keys=ON;`) yield* tx.run(`PRAGMA foreign_keys=ON;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260622142730_simplify_session_context_epoch", id: "20260622142730_simplify_session_context_epoch",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -10,4 +10,6 @@ export default {
yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`revision\`;`) yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`revision\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260622170816_reset_v2_session_state", id: "20260622170816_reset_v2_session_state",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -12,4 +12,6 @@ export default {
yield* tx.run(`DELETE FROM \`event_sequence\`;`) yield* tx.run(`DELETE FROM \`event_sequence\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260622202450_simplify_session_input", id: "20260622202450_simplify_session_input",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -14,4 +14,6 @@ export default {
yield* tx.run(`DELETE FROM \`workspace\`;`) yield* tx.run(`DELETE FROM \`workspace\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
export default { const migration: DatabaseMigration.Migration = {
id: "20260804233008_loose_psylocke", id: "20260804233008_loose_psylocke",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -135,4 +135,6 @@ export default {
yield* tx.run(`DROP TABLE \`session_input\`;`) yield* tx.run(`DROP TABLE \`session_input\`;`)
}) })
}, },
} satisfies DatabaseMigration.Migration }
export default migration
@@ -30,12 +30,14 @@ const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const decodeValue = Schema.decodeUnknownOption(LegacyValue) const decodeValue = Schema.decodeUnknownOption(LegacyValue)
const wellKnownSourcesKey = "wellknown:sources" const wellKnownSourcesKey = "wellknown:sources"
export default { const migration: DatabaseMigration.Migration = {
id: "20260805200742_import_legacy_credentials", id: "20260805200742_import_legacy_credentials",
up(tx) { up(tx) {
return importLegacyCredentials(tx, path.join(Global.Path.data, "auth.json")) return importLegacyCredentials(tx, path.join(Global.Path.data, "auth.json"))
}, },
} satisfies DatabaseMigration.Migration }
export default migration
export function importLegacyCredentials(tx: Parameters<DatabaseMigration.Migration["up"]>[0], filepath: string) { export function importLegacyCredentials(tx: Parameters<DatabaseMigration.Migration["up"]>[0], filepath: string) {
return Effect.gen(function* () { return Effect.gen(function* () {
+4 -2
View File
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "./migration" import type { DatabaseMigration } from "./migration"
export default { const schema: Omit<DatabaseMigration.Migration, "id"> = {
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(` yield* tx.run(`
@@ -248,4 +248,6 @@ export default {
) )
}) })
}, },
} satisfies Omit<DatabaseMigration.Migration, "id"> }
export default schema
+1 -1
View File
@@ -18,7 +18,7 @@ const Files = Schema.Array(File)
const key = Instructions.Key.make("core/instructions") const key = Instructions.Key.make("core/instructions")
export interface Interface { export interface Interface {
readonly load: () => Effect.Effect<Instructions.Instructions> readonly load: () => Effect.Effect<Instructions.List>
} }
export const Options = Schema.Struct({ export const Options = Schema.Struct({
+1 -1
View File
@@ -8,7 +8,7 @@ import { SessionSchema } from "../session/schema"
import { Instructions } from "./index" import { Instructions } from "./index"
export interface Interface { export interface Interface {
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions> readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.List>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionBuiltIns") {} export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionBuiltIns") {}
+7 -7
View File
@@ -53,7 +53,7 @@ export declare namespace Source {
} }
/** Ordered sources; identical values render identical bytes. */ /** Ordered sources; identical values render identical bytes. */
export type Instructions = ReadonlyArray<Source> export type List = ReadonlyArray<Source>
export type ReadResult = ReadonlyArray<{ export type ReadResult = ReadonlyArray<{
readonly key: Key readonly key: Key
@@ -82,10 +82,10 @@ export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError
} }
} }
export const empty: Instructions = [] export const empty: List = []
/** Closes a typed definition into one `Source`, so differently typed sources compose. */ /** Closes a typed definition into one `Source`, so differently typed sources compose. */
export function make<A>(source: Source.Definition<A>): Instructions { export function make<A>(source: Source.Definition<A>): List {
const decode = Schema.decodeUnknownOption(source.codec) const decode = Schema.decodeUnknownOption(source.codec)
const encode = Schema.encodeSync(source.codec) const encode = Schema.encodeSync(source.codec)
const initial = (value: A) => requireText(source.key, "initial", source.render.initial(value)) const initial = (value: A) => requireText(source.key, "initial", source.render.initial(value))
@@ -121,7 +121,7 @@ export function make<A>(source: Source.Definition<A>): Instructions {
] ]
} }
export function combine(values: ReadonlyArray<Instructions>): Instructions { export function combine(values: ReadonlyArray<List>): List {
const sources = values.flat() const sources = values.flat()
const keys = new Set<Key>() const keys = new Set<Key>()
for (const source of sources) { for (const source of sources) {
@@ -131,7 +131,7 @@ export function combine(values: ReadonlyArray<Instructions>): Instructions {
return sources return sources
} }
export function read(value: Instructions): Effect.Effect<ReadResult> { export function read(value: List): Effect.Effect<ReadResult> {
return Effect.forEach( return Effect.forEach(
value, value,
(source) => source.read.pipe(Effect.map((observed) => ({ key: source.key, value: observed }))), (source) => source.read.pipe(Effect.map((observed) => ({ key: source.key, value: observed }))),
@@ -158,7 +158,7 @@ export function diff(observed: ReadResult, previous?: Values): Effect.Effect<Adm
return Effect.succeed({ delta, blobs }) return Effect.succeed({ delta, blobs })
} }
export function renderInitial(value: Instructions, values: Readonly<Record<string, Schema.Json>>) { export function renderInitial(value: List, values: Readonly<Record<string, Schema.Json>>) {
return render( return render(
value.flatMap((source) => { value.flatMap((source) => {
if (!Object.hasOwn(values, source.key)) return [] if (!Object.hasOwn(values, source.key)) return []
@@ -169,7 +169,7 @@ export function renderInitial(value: Instructions, values: Readonly<Record<strin
} }
export function renderUpdate( export function renderUpdate(
value: Instructions, value: List,
previous: Readonly<Record<string, Schema.Json>>, previous: Readonly<Record<string, Schema.Json>>,
delta: Readonly<Record<string, Option.Option<Schema.Json>>>, delta: Readonly<Record<string, Option.Option<Schema.Json>>>,
) { ) {
+1 -1
View File
@@ -55,7 +55,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
} }
export interface Interface { export interface Interface {
readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.Instructions> readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.List>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/McpInstructions") {} export class Service extends Context.Service<Service, Interface>()("@opencode/McpInstructions") {}
+1 -1
View File
@@ -54,7 +54,7 @@ const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyA
} }
export interface Interface { export interface Interface {
readonly load: () => Effect.Effect<Instructions.Instructions> readonly load: () => Effect.Effect<Instructions.List>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/ReferenceInstructions") {} export class Service extends Context.Service<Service, Interface>()("@opencode/ReferenceInstructions") {}
+2
View File
@@ -11,6 +11,7 @@ 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 { SessionModelHeaders } from "./model-headers"
import { SessionPromptCacheKey } from "./prompt-cache-key"
import { App } from "../app" import { App } from "../app"
import { SessionRunnerModel } from "./runner/model" import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
@@ -258,6 +259,7 @@ const make = (dependencies: Dependencies) => {
.stream( .stream(
LLM.request({ LLM.request({
model: plan.model, model: plan.model,
promptCacheKey: SessionPromptCacheKey.make(plan.session.id),
http: { headers: SessionModelHeaders.make(plan.session, dependencies.app) }, http: { headers: SessionModelHeaders.make(plan.session, dependencies.app) },
messages: [Message.user(plan.prompt)], messages: [Message.user(plan.prompt)],
tools: [], tools: [],
+1 -1
View File
@@ -25,7 +25,7 @@ import { SessionStore } from "./store"
export interface Selection { export interface Selection {
readonly session: SessionSchema.Info readonly session: SessionSchema.Info
readonly agent: Agent.Selection & { readonly info: Agent.Info } readonly agent: Agent.Selection & { readonly info: Agent.Info }
readonly instructions: Instructions.Instructions readonly instructions: Instructions.List
readonly tools: Tool.Snapshot readonly tools: Tool.Snapshot
} }
+2 -4
View File
@@ -11,6 +11,7 @@ import { SessionContext } from "./context"
import { SessionGenerate } from "./generate" import { SessionGenerate } from "./generate"
import { SessionHistory } from "./history" import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers" import { SessionModelHeaders } from "./model-headers"
import { SessionPromptCacheKey } from "./prompt-cache-key"
import { SessionRunnerModel } from "./runner/model" import { SessionRunnerModel } from "./runner/model"
import PROMPT_DEFAULT from "./runner/prompt/base.txt" import PROMPT_DEFAULT from "./runner/prompt/base.txt"
import { toLLMMessages } from "./runner/to-llm-message" import { toLLMMessages } from "./runner/to-llm-message"
@@ -31,9 +32,6 @@ export const layer = Layer.effect(
const model = yield* models.resolve(selection.session) const model = yield* models.resolve(selection.session)
const history = yield* SessionHistory.preview(database.db, selection.session.id, selection.instructions) const history = yield* SessionHistory.preview(database.db, selection.session.id, selection.instructions)
const providerMetadataKey = model.model.route.providerMetadataKey ?? model.model.provider 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 tools = selection.tools
const toolDefinitions = tools.definitions const toolDefinitions = tools.definitions
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
@@ -71,7 +69,7 @@ export const layer = Layer.effect(
LLM.request({ LLM.request({
model: model.model, model: model.model,
http: { headers: SessionModelHeaders.make(selection.session, app) }, http: { headers: SessionModelHeaders.make(selection.session, app) },
providerOptions: { [providerMetadataKey]: { promptCacheKey } }, promptCacheKey: SessionPromptCacheKey.make(selection.session.id),
system: contextEvent.system, system: contextEvent.system,
messages: contextEvent.messages, messages: contextEvent.messages,
tools: hookedTools, tools: hookedTools,
+2 -2
View File
@@ -74,7 +74,7 @@ export const load = Effect.fn("SessionHistory.load")(function* (db: DatabaseServ
export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(function* ( export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(function* (
db: DatabaseService, db: DatabaseService,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
instructions: Instructions.Instructions, instructions: Instructions.List,
) { ) {
return yield* db return yield* db
.transaction(() => .transaction(() =>
@@ -92,7 +92,7 @@ export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(fun
export const preview = Effect.fn("SessionHistory.preview")(function* ( export const preview = Effect.fn("SessionHistory.preview")(function* (
db: DatabaseService, db: DatabaseService,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
instructions: Instructions.Instructions, instructions: Instructions.List,
) { ) {
const observed = yield* Instructions.read(instructions) const observed = yield* Instructions.read(instructions)
return yield* db return yield* db
@@ -25,7 +25,7 @@ export interface Interface {
}) => Effect.Effect<void, InstructionEntry.ValueTooLargeError> }) => Effect.Effect<void, InstructionEntry.ValueTooLargeError>
readonly remove: (input: { readonly sessionID: SessionSchema.ID; readonly key: Key }) => Effect.Effect<void> readonly remove: (input: { readonly sessionID: SessionSchema.ID; readonly key: Key }) => Effect.Effect<void>
/** Produces one Instructions source per stored entry, keyed `api/<key>`. */ /** Produces one Instructions source per stored entry, keyed `api/<key>`. */
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions> readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.List>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionEntry") {} export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionEntry") {}
@@ -20,7 +20,7 @@ export interface Observation extends Instructions.Admission {
export const observe = Effect.fn("InstructionState.observe")(function* ( export const observe = Effect.fn("InstructionState.observe")(function* (
db: DatabaseService, db: DatabaseService,
instructions: Instructions.Instructions, instructions: Instructions.List,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
): Effect.fn.Return<Observation, Instructions.InitializationBlocked> { ): Effect.fn.Return<Observation, Instructions.InitializationBlocked> {
const [observed, stored] = yield* Effect.all([Instructions.read(instructions), find(db, sessionID)], { const [observed, stored] = yield* Effect.all([Instructions.read(instructions), find(db, sessionID)], {
@@ -38,7 +38,7 @@ export const observe = Effect.fn("InstructionState.observe")(function* (
export const commit = Effect.fn("InstructionState.commit")(function* ( export const commit = Effect.fn("InstructionState.commit")(function* (
db: DatabaseService, db: DatabaseService,
bus: Bus.Interface, bus: Bus.Interface,
instructions: Instructions.Instructions, instructions: Instructions.List,
observation: Observation, observation: Observation,
) { ) {
if (!observation.initial && Object.keys(observation.delta).length === 0) return if (!observation.initial && Object.keys(observation.delta).length === 0) return
@@ -62,7 +62,7 @@ export const commit = Effect.fn("InstructionState.commit")(function* (
const renderUpdateText = Effect.fnUntraced(function* ( const renderUpdateText = Effect.fnUntraced(function* (
db: DatabaseService, db: DatabaseService,
instructions: Instructions.Instructions, instructions: Instructions.List,
observation: Observation, observation: Observation,
) { ) {
const replaced = Object.entries(observation.previous).filter(([key]) => Object.hasOwn(observation.delta, key)) const replaced = Object.entries(observation.previous).filter(([key]) => Object.hasOwn(observation.delta, key))
@@ -77,7 +77,7 @@ const renderUpdateText = Effect.fnUntraced(function* (
export const prepare = Effect.fn("InstructionState.prepare")(function* ( export const prepare = Effect.fn("InstructionState.prepare")(function* (
db: DatabaseService, db: DatabaseService,
bus: Bus.Interface, bus: Bus.Interface,
instructions: Instructions.Instructions, instructions: Instructions.List,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
) { ) {
yield* commit(db, bus, instructions, yield* observe(db, instructions, sessionID)) yield* commit(db, bus, instructions, yield* observe(db, instructions, sessionID))
@@ -162,7 +162,7 @@ export const reset = Effect.fn("InstructionState.reset")(function* (db: Database
export const initial = Effect.fn("InstructionState.initial")(function* ( export const initial = Effect.fn("InstructionState.initial")(function* (
db: DatabaseService, db: DatabaseService,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
instructions: Instructions.Instructions, instructions: Instructions.List,
) { ) {
const state = yield* find(db, sessionID) const state = yield* find(db, sessionID)
if (!state) return yield* Effect.die(new Error(`Instruction state not found during assembly: ${sessionID}`)) if (!state) return yield* Effect.die(new Error(`Instruction state not found during assembly: ${sessionID}`))
@@ -181,7 +181,7 @@ export const current = Effect.fn("InstructionState.current")(function* (
export const preview = Effect.fn("InstructionState.preview")(function* ( export const preview = Effect.fn("InstructionState.preview")(function* (
db: DatabaseService, db: DatabaseService,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
instructions: Instructions.Instructions, instructions: Instructions.List,
observed: Instructions.ReadResult, observed: Instructions.ReadResult,
) { ) {
const state = yield* find(db, sessionID) const state = yield* find(db, sessionID)
+2 -2
View File
@@ -15,6 +15,7 @@ import { QuestionTool } from "../tool/plugin/question"
import { Tool } from "../tool" import { Tool } from "../tool"
import { SessionContext } from "./context" import { SessionContext } from "./context"
import { SessionModelHeaders } from "./model-headers" import { SessionModelHeaders } from "./model-headers"
import { SessionPromptCacheKey } from "./prompt-cache-key"
import { PromptCacheDiagnostics } from "./prompt-cache-diagnostics" import { PromptCacheDiagnostics } from "./prompt-cache-diagnostics"
import { MAX_STEPS_PROMPT } from "./runner/max-steps" import { MAX_STEPS_PROMPT } from "./runner/max-steps"
import PROMPT_DEFAULT from "./runner/prompt/base.txt" import PROMPT_DEFAULT from "./runner/prompt/base.txt"
@@ -181,7 +182,6 @@ export const layer = Layer.effect(
// The final Step keeps definitions available to protocols with native "none", // The final Step keeps definitions available to protocols with native "none",
// preserving their prompt cache prefix. Calls are still rejected at execution. // preserving their prompt cache prefix. Calls are still rejected at execution.
const tools = input.context.tools 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] const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial]
.filter((part) => part.length > 0) .filter((part) => part.length > 0)
.map(SystemPart.make) .map(SystemPart.make)
@@ -220,7 +220,7 @@ export const layer = Layer.effect(
http: { http: {
headers: SessionModelHeaders.make(session, app), headers: SessionModelHeaders.make(session, app),
}, },
providerOptions: { [providerMetadataKey]: { promptCacheKey } }, promptCacheKey: SessionPromptCacheKey.make(session.id),
system: context.system, system: context.system,
messages: boundImages(unsupportedParts(context.messages, resolved.capabilities)), messages: boundImages(unsupportedParts(context.messages, resolved.capabilities)),
tools: Array.from(hooked, ([name, tool]) => ({ ...tool, name })), tools: Array.from(hooked, ([name, tool]) => ({ ...tool, name })),
@@ -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
+1 -1
View File
@@ -57,7 +57,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
} }
export interface Interface { export interface Interface {
readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.Instructions> readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.List>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/SkillInstructions") {} export class Service extends Context.Service<Service, Interface>()("@opencode/SkillInstructions") {}
+1 -1
View File
@@ -22,7 +22,7 @@ export abstract class NamedError extends Error {
return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data)) return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data))
} }
private static createSchemaClass<Name extends string, DataSchema extends Schema.Top>(name: Name, data: DataSchema) { public static createSchemaClass<Name extends string, DataSchema extends Schema.Top>(name: Name, data: DataSchema) {
const schema = Schema.Struct({ const schema = Schema.Struct({
name: Schema.Literal(name), name: Schema.Literal(name),
data, data,
-4
View File
@@ -179,7 +179,6 @@ describe("AISDKNative", () => {
models: ["anthropic/claude-sonnet-4.6"], models: ["anthropic/claude-sonnet-4.6"],
provider: { only: ["anthropic"], require_parameters: true }, provider: { only: ["anthropic"], require_parameters: true },
reasoning: { effort: "high" }, reasoning: { effort: "high" },
promptCacheKey: "session_123",
future_option: { enabled: true }, future_option: { enabled: true },
}), }),
).toEqual({ ).toEqual({
@@ -190,7 +189,6 @@ describe("AISDKNative", () => {
models: ["anthropic/claude-sonnet-4.6"], models: ["anthropic/claude-sonnet-4.6"],
provider: { only: ["anthropic"], require_parameters: true }, provider: { only: ["anthropic"], require_parameters: true },
reasoning: { effort: "high" }, reasoning: { effort: "high" },
promptCacheKey: "session_123",
future_option: { enabled: true }, future_option: { enabled: true },
}, },
}, },
@@ -271,7 +269,6 @@ describe("AISDKNative", () => {
baseURL: "https://xai.example/v1", baseURL: "https://xai.example/v1",
reasoningEffort: "custom", reasoningEffort: "custom",
store: true, store: true,
promptCacheKey: "cache-key",
}), }),
).toEqual({ ).toEqual({
package: "@opencode-ai/ai/providers/xai", package: "@opencode-ai/ai/providers/xai",
@@ -282,7 +279,6 @@ describe("AISDKNative", () => {
xai: { xai: {
reasoningEffort: "custom", reasoningEffort: "custom",
store: true, store: true,
promptCacheKey: "cache-key",
}, },
}, },
}, },
+1 -1
View File
@@ -68,7 +68,7 @@ const instructionEvents = (db: Database.Interface["db"], sessionID: SessionSchem
.all() .all()
.pipe(Effect.orDie) .pipe(Effect.orDie)
const preview = (db: Database.Interface["db"], sessionID: SessionSchema.ID, instructions: Instructions.Instructions) => const preview = (db: Database.Interface["db"], sessionID: SessionSchema.ID, instructions: Instructions.List) =>
Instructions.read(instructions).pipe( Instructions.read(instructions).pipe(
Effect.flatMap((observed) => InstructionState.preview(db, sessionID, instructions, observed)), Effect.flatMap((observed) => InstructionState.preview(db, sessionID, instructions, observed)),
) )

Some files were not shown because too many files have changed in this diff Show More