mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-07 17:49:53 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b84e24e65 | |||
| 3776975d5c | |||
| d2c99ba97c | |||
| 6f3a3600b9 | |||
| 9ca650f97c |
@@ -368,11 +368,12 @@ Other provider exports listed above remain direct facades until they explicitly
|
||||
|
||||
## 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).
|
||||
2. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `promptCacheKey`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
|
||||
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.
|
||||
2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it.
|
||||
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.
|
||||
|
||||
|
||||
@@ -33,9 +33,10 @@ const model = OpenAI.configure({
|
||||
//
|
||||
// - `generation`: common controls such as max tokens, temperature, topP/topK,
|
||||
// penalties, seed, and stop sequences.
|
||||
// - `promptCacheKey`: stable cache affinity for protocols that support it.
|
||||
// - `providerOptions`: namespaced provider-native behavior. For example,
|
||||
// OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking
|
||||
// config, or OpenRouter routing/reasoning.
|
||||
// OpenAI store behavior, Anthropic thinking, Gemini thinking config, or
|
||||
// OpenRouter routing/reasoning.
|
||||
// - `http`: last-resort serializable overlays for final request body, headers,
|
||||
// and query params. Prefer typed `providerOptions` when a field is stable.
|
||||
//
|
||||
@@ -45,9 +46,7 @@ const request = LLM.request({
|
||||
system: "You are concise and practical.",
|
||||
prompt: "Tell me a joke",
|
||||
generation: { maxTokens: 80, temperature: 0.7 },
|
||||
providerOptions: {
|
||||
openai: { promptCacheKey: "tutorial-joke" },
|
||||
},
|
||||
promptCacheKey: "tutorial-joke",
|
||||
})
|
||||
|
||||
// 3. `generate` sends the request and collects the event stream into one
|
||||
|
||||
@@ -90,15 +90,10 @@ const OpenResponsesFunctionCallOutput = 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("user"),
|
||||
id: Schema.optionalKey(Schema.String),
|
||||
content: Schema.Array(OpenResponsesInputContent),
|
||||
}),
|
||||
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
|
||||
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }),
|
||||
Schema.Struct({
|
||||
role: Schema.tag("assistant"),
|
||||
id: Schema.optionalKey(Schema.String),
|
||||
content: Schema.Array(OpenResponsesOutputText),
|
||||
phase: Schema.optionalKey(MessagePhase),
|
||||
}),
|
||||
@@ -106,23 +101,19 @@ export const InputItem = Schema.Union([
|
||||
OpenResponsesItemReference,
|
||||
Schema.Struct({
|
||||
type: Schema.tag("function_call"),
|
||||
id: Schema.optionalKey(Schema.String),
|
||||
call_id: Schema.String,
|
||||
name: Schema.String,
|
||||
arguments: Schema.String,
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.tag("function_call_output"),
|
||||
id: Schema.optionalKey(Schema.String),
|
||||
call_id: Schema.String,
|
||||
output: OpenResponsesFunctionCallOutput,
|
||||
}),
|
||||
])
|
||||
type OpenResponsesInputItem = Schema.Schema.Type<typeof InputItem>
|
||||
type ProviderInputItem = Readonly<Record<string, unknown>> & { readonly type: string; readonly id?: string }
|
||||
type LoweredInputItem =
|
||||
| OpenResponsesInputItem
|
||||
| ProviderInputItem
|
||||
| {
|
||||
readonly role: "assistant"
|
||||
readonly content: ReadonlyArray<{ readonly type: "output_text"; readonly text: string }>
|
||||
@@ -137,7 +128,7 @@ type OpenResponsesReasoningInput = {
|
||||
summary: Array<{ type: "summary_text"; text: string }>
|
||||
encrypted_content?: string | null
|
||||
}
|
||||
type OpenResponsesReasoningReplay = Omit<OpenResponsesReasoningInput, "id"> & { id?: string }
|
||||
type OpenResponsesReasoningReplay = Omit<OpenResponsesReasoningInput, "id">
|
||||
|
||||
export const Tool = Schema.Struct({
|
||||
type: Schema.tag("function"),
|
||||
@@ -263,11 +254,6 @@ export interface Extension {
|
||||
readonly request: LLMRequest
|
||||
}) => MediaInput | 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 }
|
||||
@@ -324,17 +310,6 @@ export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable<LL
|
||||
tool: (toolName) => ({ type: "function" as const, name: toolName }),
|
||||
})
|
||||
|
||||
const metadataItemID = (
|
||||
part: { readonly itemId?: string; readonly providerMetadata?: ProviderMetadata },
|
||||
providerMetadataKey: string,
|
||||
) => {
|
||||
if (part.itemId) return part.itemId
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
return ProviderShared.isRecord(metadata) && typeof metadata.itemId === "string" && metadata.itemId.length > 0
|
||||
? metadata.itemId
|
||||
: undefined
|
||||
}
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart): OpenResponsesInputItem => ({
|
||||
type: "function_call",
|
||||
call_id: part.id,
|
||||
@@ -344,23 +319,26 @@ const lowerToolCall = (part: ToolCallPart): OpenResponsesInputItem => ({
|
||||
|
||||
const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
const itemId = metadataItemID(part, providerMetadataKey)
|
||||
if (!itemId) return undefined
|
||||
if (!ProviderShared.isRecord(metadata) || typeof metadata.itemId !== "string" || metadata.itemId.length === 0)
|
||||
return undefined
|
||||
const encryptedContent =
|
||||
ProviderShared.isRecord(metadata) &&
|
||||
(typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null)
|
||||
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
|
||||
? metadata.reasoningEncryptedContent
|
||||
: undefined
|
||||
return {
|
||||
type: "reasoning",
|
||||
id: itemId,
|
||||
id: metadata.itemId,
|
||||
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
|
||||
encrypted_content: encryptedContent,
|
||||
}
|
||||
}
|
||||
|
||||
const hostedToolItemID = (part: ToolResultPart, providerMetadataKey: string) =>
|
||||
metadataItemID(part, providerMetadataKey)
|
||||
const hostedToolItemID = (part: ToolResultPart, providerMetadataKey: string) => {
|
||||
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* (
|
||||
part: MediaPart,
|
||||
@@ -419,18 +397,17 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request: LLMRequest, extension: Extension) {
|
||||
const options = OpenResponsesOptions.resolve(request)
|
||||
const system: LoweredInputItem[] =
|
||||
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
|
||||
const input: LoweredInputItem[] = [...system]
|
||||
const store = options.store
|
||||
const store = OpenResponsesOptions.resolve(request).store
|
||||
const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses"
|
||||
|
||||
for (const message of request.messages) {
|
||||
if (message.role === "system") {
|
||||
const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message)
|
||||
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] = {
|
||||
role: "user",
|
||||
content: [...previous.content, { type: "input_text", text: part.text }],
|
||||
@@ -450,24 +427,24 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
|
||||
if (message.role === "assistant") {
|
||||
const content: TextPart[] = []
|
||||
const reasoningItems: Record<string, OpenResponsesReasoningReplay> = {}
|
||||
const hostedToolItems = new Set<string>()
|
||||
const reasoningReferences = new Set<string>()
|
||||
const hostedToolReferences = new Set<string>()
|
||||
const flushText = () => {
|
||||
if (content.length === 0) return
|
||||
const groups = content.reduce<
|
||||
Array<{ phase: MessagePhase | null | undefined; itemId: string | undefined; parts: TextPart[] }>
|
||||
>((groups, part) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
const phase = ProviderShared.isRecord(metadata) ? messagePhase(metadata.phase, extension) : undefined
|
||||
const itemId = metadataItemID(part, providerMetadataKey)
|
||||
const group = groups.at(-1)
|
||||
if (group && group.phase === phase && group.itemId === itemId) group.parts.push(part)
|
||||
else groups.push({ phase, itemId, parts: [part] })
|
||||
return groups
|
||||
}, [])
|
||||
const groups = content.reduce<Array<{ phase: MessagePhase | null | undefined; parts: TextPart[] }>>(
|
||||
(groups, part) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
const phase = ProviderShared.isRecord(metadata) ? messagePhase(metadata.phase, extension) : undefined
|
||||
const group = groups.at(-1)
|
||||
if (group && group.phase === phase) group.parts.push(part)
|
||||
else groups.push({ phase, parts: [part] })
|
||||
return groups
|
||||
},
|
||||
[],
|
||||
)
|
||||
input.push(
|
||||
...groups.map((group) => ({
|
||||
role: "assistant" as const,
|
||||
...(group.itemId === undefined ? {} : { id: group.itemId }),
|
||||
content: group.parts.map((part) => ({ type: "output_text" as const, text: part.text })),
|
||||
...(group.phase === undefined ? {} : { phase: group.phase }),
|
||||
})),
|
||||
@@ -483,6 +460,11 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
|
||||
flushText()
|
||||
const reasoning = lowerReasoning(part, providerMetadataKey)
|
||||
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]
|
||||
if (existing) {
|
||||
existing.summary.push(...reasoning.summary)
|
||||
@@ -492,7 +474,6 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
|
||||
}
|
||||
const replay = {
|
||||
type: reasoning.type,
|
||||
id: reasoning.id,
|
||||
summary: reasoning.summary,
|
||||
encrypted_content: reasoning.encrypted_content,
|
||||
}
|
||||
@@ -509,18 +490,16 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
|
||||
if (part.type === "tool-result" && part.providerExecuted === true) {
|
||||
flushText()
|
||||
const itemID = hostedToolItemID(part, providerMetadataKey)
|
||||
const providerItem = extension.lowerProviderItem?.(part, providerMetadataKey, store)
|
||||
if (providerItem && itemID && !hostedToolItems.has(itemID)) input.push(providerItem)
|
||||
if (!providerItem && store !== false && itemID && !hostedToolItems.has(itemID))
|
||||
if (store !== false && itemID && !hostedToolReferences.has(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
|
||||
input.push({
|
||||
role: "user",
|
||||
content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)),
|
||||
})
|
||||
}
|
||||
if (itemID) hostedToolItems.add(itemID)
|
||||
if (itemID) hostedToolReferences.add(itemID)
|
||||
continue
|
||||
}
|
||||
return yield* ProviderShared.unsupportedContent(extension.name, "assistant", [
|
||||
@@ -560,7 +539,7 @@ const lowerOptions = (request: LLMRequest) => {
|
||||
return {
|
||||
...(options.instructions ? { instructions: options.instructions } : {}),
|
||||
...(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.reasoningEffort || options.reasoningSummary
|
||||
? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
|
||||
@@ -662,9 +641,9 @@ const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepRe
|
||||
if (!event.delta) return [state, NO_EVENTS]
|
||||
const events: LLMEvent[] = []
|
||||
const phase = state.messagePhases[id]
|
||||
const metadata = providerMetadata(state, { itemId: id, ...(phase === undefined ? {} : { phase }) })
|
||||
const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata, id)
|
||||
return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta, metadata, id) }, events]
|
||||
const metadata = phase === undefined ? undefined : providerMetadata(state, { phase })
|
||||
const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata)
|
||||
return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) }, events]
|
||||
}
|
||||
|
||||
const onOutputTextDone = (state: ParserState, event: Event, id: string): StepResult => {
|
||||
@@ -673,13 +652,7 @@ const onOutputTextDone = (state: ParserState, event: Event, id: string): StepRes
|
||||
return onOutputTextDelta(state, { ...event, delta: event.text }, id)
|
||||
}
|
||||
const events: LLMEvent[] = []
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.textEnd(state.lifecycle, events, id, providerMetadata(state, { itemId: id }), id),
|
||||
},
|
||||
events,
|
||||
]
|
||||
return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, id) }, events]
|
||||
}
|
||||
|
||||
export const onReasoningDelta = (state: ParserState, event: Event, itemID: string): StepResult => {
|
||||
@@ -690,14 +663,7 @@ export const onReasoningDelta = (state: ParserState, event: Event, itemID: strin
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.reasoningDelta(
|
||||
state.lifecycle,
|
||||
events,
|
||||
id,
|
||||
event.delta,
|
||||
providerMetadata(state, { itemId: itemID }),
|
||||
itemID,
|
||||
),
|
||||
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
|
||||
},
|
||||
events,
|
||||
]
|
||||
@@ -739,13 +705,7 @@ const onOutputItemAdded = (state: ParserState, event: Event): StepResult => {
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.reasoningStart(
|
||||
state.lifecycle,
|
||||
events,
|
||||
`${item.id}:0`,
|
||||
reasoningMetadata(state, item),
|
||||
item.id,
|
||||
),
|
||||
lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(state, item)),
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
[item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
|
||||
@@ -764,7 +724,6 @@ const onOutputItemAdded = (state: ParserState, event: Event): StepResult => {
|
||||
lifecycle,
|
||||
tools: ToolStream.start(state.tools, item.id, {
|
||||
id: item.call_id ?? item.id,
|
||||
itemId: item.id,
|
||||
name: item.name ?? "",
|
||||
input: item.arguments ?? "",
|
||||
providerMetadata: metadata,
|
||||
@@ -772,12 +731,7 @@ const onOutputItemAdded = (state: ParserState, event: Event): StepResult => {
|
||||
},
|
||||
[
|
||||
...events,
|
||||
LLMEvent.toolInputStart({
|
||||
id: item.call_id ?? item.id,
|
||||
itemId: item.id,
|
||||
name: item.name ?? "",
|
||||
providerMetadata: metadata,
|
||||
}),
|
||||
LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata: metadata }),
|
||||
],
|
||||
]
|
||||
}
|
||||
@@ -796,7 +750,6 @@ const onReasoningSummaryPartAdded = (state: ParserState, event: Event): StepResu
|
||||
events,
|
||||
`${event.item_id}:0`,
|
||||
providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: null }),
|
||||
event.item_id,
|
||||
),
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
@@ -817,7 +770,6 @@ const onReasoningSummaryPartAdded = (state: ParserState, event: Event): StepResu
|
||||
events,
|
||||
`${event.item_id}:${entry[0]}`,
|
||||
providerMetadata(state, { itemId: event.item_id }),
|
||||
event.item_id,
|
||||
),
|
||||
state.lifecycle,
|
||||
)
|
||||
@@ -829,7 +781,6 @@ const onReasoningSummaryPartAdded = (state: ParserState, event: Event): StepResu
|
||||
events,
|
||||
`${event.item_id}:${event.summary_index}`,
|
||||
providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }),
|
||||
event.item_id,
|
||||
),
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
@@ -865,7 +816,6 @@ const onReasoningSummaryPartDone = (state: ParserState, event: Event): StepResul
|
||||
events,
|
||||
`${event.item_id}:${event.summary_index}`,
|
||||
providerMetadata(state, { itemId: event.item_id }),
|
||||
event.item_id,
|
||||
)
|
||||
: state.lifecycle,
|
||||
reasoningItems: {
|
||||
@@ -920,8 +870,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
state.lifecycle,
|
||||
events,
|
||||
item.id,
|
||||
providerMetadata(state, { itemId: item.id, ...(phase === undefined ? {} : { phase }) }),
|
||||
item.id,
|
||||
phase === undefined ? undefined : providerMetadata(state, { phase }),
|
||||
),
|
||||
messageItems,
|
||||
messagePhases,
|
||||
@@ -932,15 +881,9 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
|
||||
if (item.type === "function_call") {
|
||||
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]
|
||||
? state.tools
|
||||
: ToolStream.start(state.tools, item.id, {
|
||||
id: item.call_id,
|
||||
itemId: item.id,
|
||||
name: item.name,
|
||||
providerMetadata: metadata,
|
||||
})
|
||||
: ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name })
|
||||
const result =
|
||||
item.arguments === undefined
|
||||
? yield* ToolStream.finish(state.id, tools, item.id)
|
||||
@@ -970,7 +913,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
const lifecycle = Object.entries(reasoningItem.summaryParts)
|
||||
.filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
|
||||
.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,
|
||||
)
|
||||
const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems
|
||||
@@ -978,12 +921,12 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
}
|
||||
if (!state.lifecycle.reasoning.has(item.id)) {
|
||||
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
||||
events.push(LLMEvent.reasoningStart({ id: item.id, itemId: item.id, providerMetadata: metadata }))
|
||||
events.push(LLMEvent.reasoningEnd({ id: item.id, itemId: item.id, providerMetadata: metadata }))
|
||||
events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata: metadata }))
|
||||
events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata: metadata }))
|
||||
return [{ ...state, lifecycle }, events] satisfies StepResult
|
||||
}
|
||||
return [
|
||||
{ ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, metadata, item.id) },
|
||||
{ ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, metadata) },
|
||||
events,
|
||||
] satisfies StepResult
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ export const bodyFields = {
|
||||
stream: Schema.Literal(true),
|
||||
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
|
||||
store: Schema.optional(Schema.Boolean),
|
||||
prompt_cache_key: Schema.optional(Schema.String),
|
||||
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
||||
max_completion_tokens: Schema.optional(Schema.Number),
|
||||
max_tokens: Schema.optional(Schema.Number),
|
||||
@@ -509,6 +510,7 @@ const lowerOptions = (request: LLMRequest) => {
|
||||
const options = OpenAIOptions.resolve(request)
|
||||
return {
|
||||
...(options.store !== undefined ? { store: options.store } : {}),
|
||||
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
||||
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,10 @@ const OpenAIResponsesToolChoice = Schema.Union([
|
||||
const OpenAIResponsesInputItem = Schema.Union([
|
||||
Schema.Struct({
|
||||
role: Schema.tag("assistant"),
|
||||
id: Schema.optionalKey(Schema.String),
|
||||
content: Schema.Array(Schema.Struct({ type: Schema.tag("output_text"), text: Schema.String })),
|
||||
phase: Schema.optionalKey(Schema.NullOr(OpenResponses.MessagePhase)),
|
||||
}),
|
||||
OpenResponses.InputItem,
|
||||
Schema.StructWithRest(Schema.Struct({ type: Schema.String, id: Schema.optionalKey(Schema.String) }), [
|
||||
Schema.Record(Schema.String, Schema.Unknown),
|
||||
]),
|
||||
])
|
||||
|
||||
const OpenAIResponsesCoreFields = {
|
||||
@@ -84,25 +80,6 @@ const extension = {
|
||||
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
|
||||
|
||||
const nativeImageToolInput = (tool: ToolDefinition) => {
|
||||
@@ -218,29 +195,23 @@ const onHostedToolDone = Effect.fn("OpenAIResponses.onHostedToolDone")(function*
|
||||
item: HostedToolItem,
|
||||
) {
|
||||
const tool = HOSTED_TOOLS[item.type]
|
||||
const callMetadata = OpenResponses.providerMetadata(state, { itemId: item.id })
|
||||
const resultMetadata = OpenResponses.providerMetadata(
|
||||
state,
|
||||
item.type === "image_generation_call" ? { itemId: item.id } : { itemId: item.id, item },
|
||||
)
|
||||
const providerMetadata = OpenResponses.providerMetadata(state, { itemId: item.id })
|
||||
const events: LLMEvent[] = []
|
||||
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
||||
events.push(
|
||||
LLMEvent.toolCall({
|
||||
id: item.id,
|
||||
itemId: item.id,
|
||||
name: tool.name,
|
||||
input: tool.input(item),
|
||||
providerExecuted: true,
|
||||
providerMetadata: callMetadata,
|
||||
providerMetadata,
|
||||
}),
|
||||
LLMEvent.toolResult({
|
||||
id: item.id,
|
||||
itemId: item.id,
|
||||
name: tool.name,
|
||||
result: yield* hostedToolResult(item),
|
||||
providerExecuted: true,
|
||||
providerMetadata: resultMetadata,
|
||||
providerMetadata,
|
||||
}),
|
||||
)
|
||||
return [{ ...state, lifecycle }, events] satisfies OpenResponses.StepResult
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
LLMEvent,
|
||||
type FinishReasonDetails,
|
||||
type ProviderMetadata,
|
||||
type ResponseItemID,
|
||||
type Usage,
|
||||
} from "../../schema"
|
||||
import { LLMEvent, type FinishReasonDetails, type ProviderMetadata, type Usage } from "../../schema"
|
||||
|
||||
export interface State {
|
||||
readonly stepStarted: boolean
|
||||
@@ -20,29 +14,16 @@ export const stepStart = (state: State, events: LLMEvent[]): State => {
|
||||
return { ...state, stepStarted: true }
|
||||
}
|
||||
|
||||
export const textStart = (
|
||||
state: State,
|
||||
events: LLMEvent[],
|
||||
id: string,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
itemId?: ResponseItemID,
|
||||
): State => {
|
||||
export const textStart = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => {
|
||||
if (state.text.has(id)) return state
|
||||
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]) }
|
||||
}
|
||||
|
||||
export const textDelta = (
|
||||
state: State,
|
||||
events: LLMEvent[],
|
||||
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 }))
|
||||
export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
|
||||
const started = textStart(state, events, id)
|
||||
events.push(LLMEvent.textDelta({ id, text }))
|
||||
return started
|
||||
}
|
||||
|
||||
@@ -51,11 +32,10 @@ export const reasoningStart = (
|
||||
events: LLMEvent[],
|
||||
id: string,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
itemId?: ResponseItemID,
|
||||
): State => {
|
||||
if (state.reasoning.has(id)) return state
|
||||
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]) }
|
||||
}
|
||||
|
||||
@@ -65,10 +45,9 @@ export const reasoningDelta = (
|
||||
id: string,
|
||||
text: string,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
itemId?: ResponseItemID,
|
||||
): State => {
|
||||
const started = reasoningStart(state, events, id, providerMetadata, itemId)
|
||||
events.push(LLMEvent.reasoningDelta({ id, ...(itemId === undefined ? {} : { itemId }), text, providerMetadata }))
|
||||
const started = reasoningStart(state, events, id, providerMetadata)
|
||||
events.push(LLMEvent.reasoningDelta({ id, text, providerMetadata }))
|
||||
return started
|
||||
}
|
||||
|
||||
@@ -77,26 +56,19 @@ export const reasoningEnd = (
|
||||
events: LLMEvent[],
|
||||
id: string,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
itemId?: ResponseItemID,
|
||||
): State => {
|
||||
if (!state.reasoning.has(id)) return state
|
||||
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)
|
||||
reasoning.delete(id)
|
||||
return { ...stepped, reasoning }
|
||||
}
|
||||
|
||||
export const textEnd = (
|
||||
state: State,
|
||||
events: LLMEvent[],
|
||||
id: string,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
itemId?: ResponseItemID,
|
||||
): State => {
|
||||
export const textEnd = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => {
|
||||
if (!state.text.has(id)) return state
|
||||
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)
|
||||
text.delete(id)
|
||||
return { ...stepped, text }
|
||||
|
||||
@@ -33,7 +33,6 @@ export const ServiceTierSchema = Schema.Literals(ServiceTiers)
|
||||
export interface Resolved {
|
||||
readonly instructions?: string
|
||||
readonly store?: boolean
|
||||
readonly promptCacheKey?: string
|
||||
readonly reasoningEffort?: string
|
||||
readonly reasoningSummary?: "auto" | "concise" | "detailed"
|
||||
readonly include?: ReadonlyArray<ResponseIncludable>
|
||||
@@ -50,7 +49,6 @@ export const resolve = (request: LLMRequest): Resolved => {
|
||||
return {
|
||||
instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
|
||||
store: typeof input?.store === "boolean" ? input.store : undefined,
|
||||
promptCacheKey: typeof input?.promptCacheKey === "string" ? input.promptCacheKey : undefined,
|
||||
reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
|
||||
reasoningSummary:
|
||||
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import {
|
||||
AIError,
|
||||
LLMEvent,
|
||||
type ProviderMetadata,
|
||||
type ResponseItemID,
|
||||
type ToolCall,
|
||||
type ToolInputError,
|
||||
} from "../../schema"
|
||||
import { AIError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema"
|
||||
import { eventError, parseToolInput, type ToolAccumulator } from "../shared"
|
||||
|
||||
type StreamKey = string | number
|
||||
@@ -17,7 +10,6 @@ type StreamKey = string | number
|
||||
* so far, not the parsed object.
|
||||
*/
|
||||
export interface PendingTool extends ToolAccumulator {
|
||||
readonly itemId?: ResponseItemID
|
||||
readonly providerExecuted?: boolean
|
||||
readonly providerMetadata?: ProviderMetadata
|
||||
}
|
||||
@@ -60,7 +52,6 @@ const withoutTool = <K extends StreamKey>(tools: State<K>, key: K): State<K> =>
|
||||
const inputStart = (tool: PendingTool) =>
|
||||
LLMEvent.toolInputStart({
|
||||
id: tool.id,
|
||||
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
|
||||
name: tool.name,
|
||||
providerExecuted: tool.providerExecuted ? true : undefined,
|
||||
providerMetadata: tool.providerMetadata,
|
||||
@@ -69,7 +60,6 @@ const inputStart = (tool: PendingTool) =>
|
||||
const inputDelta = (tool: PendingTool, text: string) =>
|
||||
LLMEvent.toolInputDelta({
|
||||
id: tool.id,
|
||||
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
|
||||
name: tool.name,
|
||||
text,
|
||||
})
|
||||
@@ -80,7 +70,6 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
|
||||
Effect.map((input): ToolCall | ToolInputError =>
|
||||
LLMEvent.toolCall({
|
||||
id: tool.id,
|
||||
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
|
||||
name: tool.name,
|
||||
input,
|
||||
providerExecuted: tool.providerExecuted ? true : undefined,
|
||||
@@ -93,7 +82,6 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
|
||||
: Effect.succeed(
|
||||
LLMEvent.toolInputError({
|
||||
id: tool.id,
|
||||
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
|
||||
name: tool.name,
|
||||
raw,
|
||||
}),
|
||||
@@ -105,15 +93,7 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
|
||||
const finishEvents = (tool: PendingTool, event: ToolCall | ToolInputError): ReadonlyArray<LLMEvent> =>
|
||||
event.type === "tool-input-error"
|
||||
? [event]
|
||||
: [
|
||||
LLMEvent.toolInputEnd({
|
||||
id: tool.id,
|
||||
...(tool.itemId === undefined ? {} : { itemId: tool.itemId }),
|
||||
name: tool.name,
|
||||
providerMetadata: tool.providerMetadata,
|
||||
}),
|
||||
event,
|
||||
]
|
||||
: [LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }), event]
|
||||
|
||||
/** Store the updated tool and produce the optional public delta event. */
|
||||
const appendTool = <K extends StreamKey>(
|
||||
@@ -168,7 +148,6 @@ export const appendOrStart = <K extends StreamKey>(
|
||||
id,
|
||||
name,
|
||||
input: `${current?.input ?? ""}${delta.text}`,
|
||||
itemId: current?.itemId,
|
||||
providerExecuted: current?.providerExecuted,
|
||||
providerMetadata: current?.providerMetadata,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface OpenResponsesOptionsInput {
|
||||
readonly [key: string]: unknown
|
||||
readonly instructions?: string
|
||||
readonly store?: boolean
|
||||
readonly promptCacheKey?: string
|
||||
readonly reasoningEffort?: ReasoningEffort
|
||||
readonly reasoningSummary?: "auto" | "concise" | "detailed"
|
||||
readonly include?: ReadonlyArray<ResponseIncludable>
|
||||
|
||||
@@ -17,7 +17,6 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
|
||||
const openai = Object.fromEntries(
|
||||
definedEntries({
|
||||
store: options?.store,
|
||||
promptCacheKey: options?.promptCacheKey,
|
||||
reasoningEffort: options?.reasoningEffort,
|
||||
reasoningSummary: options?.reasoningSummary,
|
||||
include: options?.include,
|
||||
|
||||
@@ -55,7 +55,6 @@ export interface OpenRouterOptions {
|
||||
readonly debug?: Readonly<{ echo_upstream_body?: boolean }>
|
||||
readonly models?: ReadonlyArray<string>
|
||||
readonly plugins?: ReadonlyArray<OpenRouterPlugin>
|
||||
readonly promptCacheKey?: string
|
||||
readonly provider?: OpenRouterProviderRouting
|
||||
readonly reasoning?: Readonly<{
|
||||
enabled?: boolean
|
||||
@@ -122,6 +121,7 @@ export const protocol = Protocol.make({
|
||||
...body,
|
||||
messages,
|
||||
...bodyOptions(request.providerOptions?.openrouter),
|
||||
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
||||
} as OpenRouterBody
|
||||
}),
|
||||
),
|
||||
@@ -161,7 +161,6 @@ const bodyOptions = (input: unknown) => {
|
||||
...(isRecord(debug) ? { debug } : {}),
|
||||
...(typeof user === "string" ? { user } : {}),
|
||||
...(isRecord(reasoning) ? { reasoning } : {}),
|
||||
...(typeof promptCacheKey === "string" ? { prompt_cache_key: promptCacheKey } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ const chatRoute = Route.make({
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }),
|
||||
transport: OpenAICompatibleChat.route.transport,
|
||||
headers: ({ request }): Record<string, string> =>
|
||||
request.promptCacheKey ? { "x-grok-conv-id": request.promptCacheKey } : {},
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { ProviderFailureClassification } from "./errors"
|
||||
|
||||
@@ -84,7 +84,6 @@ export type StepStart = Schema.Schema.Type<typeof StepStart>
|
||||
export const TextStart = Schema.Struct({
|
||||
type: Schema.tag("text-start"),
|
||||
id: ContentBlockID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.TextStart" })
|
||||
export type TextStart = Schema.Schema.Type<typeof TextStart>
|
||||
@@ -93,7 +92,6 @@ export const TextDelta = Schema.Struct({
|
||||
type: Schema.tag("text-delta"),
|
||||
id: ContentBlockID,
|
||||
text: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.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({
|
||||
type: Schema.tag("text-end"),
|
||||
id: ContentBlockID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.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({
|
||||
type: Schema.tag("reasoning-start"),
|
||||
id: ContentBlockID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.ReasoningStart" })
|
||||
export type ReasoningStart = Schema.Schema.Type<typeof ReasoningStart>
|
||||
@@ -118,7 +114,6 @@ export const ReasoningDelta = Schema.Struct({
|
||||
type: Schema.tag("reasoning-delta"),
|
||||
id: ContentBlockID,
|
||||
text: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.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({
|
||||
type: Schema.tag("reasoning-end"),
|
||||
id: ContentBlockID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.ReasoningEnd" })
|
||||
export type ReasoningEnd = Schema.Schema.Type<typeof ReasoningEnd>
|
||||
@@ -135,7 +129,6 @@ export const ToolInputStart = Schema.Struct({
|
||||
type: Schema.tag("tool-input-start"),
|
||||
id: ToolCallID,
|
||||
name: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerExecuted: Schema.optional(Schema.Boolean),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.ToolInputStart" })
|
||||
@@ -144,7 +137,6 @@ export type ToolInputStart = Schema.Schema.Type<typeof ToolInputStart>
|
||||
export const ToolInputDelta = Schema.Struct({
|
||||
type: Schema.tag("tool-input-delta"),
|
||||
id: ToolCallID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
name: Schema.String,
|
||||
text: Schema.String,
|
||||
}).annotate({ identifier: "LLM.Event.ToolInputDelta" })
|
||||
@@ -154,7 +146,6 @@ export const ToolInputEnd = Schema.Struct({
|
||||
type: Schema.tag("tool-input-end"),
|
||||
id: ToolCallID,
|
||||
name: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "LLM.Event.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({
|
||||
type: Schema.tag("tool-input-error"),
|
||||
id: ToolCallID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
name: Schema.String,
|
||||
raw: Schema.String,
|
||||
}).annotate({ identifier: "LLM.Event.ToolInputError" })
|
||||
@@ -172,7 +162,6 @@ export type ToolInputError = Schema.Schema.Type<typeof ToolInputError>
|
||||
export const ToolCall = Schema.Struct({
|
||||
type: Schema.tag("tool-call"),
|
||||
id: ToolCallID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
name: Schema.String,
|
||||
input: Schema.Unknown,
|
||||
providerExecuted: Schema.optional(Schema.Boolean),
|
||||
@@ -183,7 +172,6 @@ export type ToolCall = Schema.Schema.Type<typeof ToolCall>
|
||||
export const ToolResult = Schema.Struct({
|
||||
type: Schema.tag("tool-result"),
|
||||
id: ToolCallID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
name: Schema.String,
|
||||
result: ToolResultValue,
|
||||
output: Schema.optional(ToolOutput),
|
||||
@@ -195,7 +183,6 @@ export type ToolResult = Schema.Schema.Type<typeof ToolResult>
|
||||
export const ToolError = Schema.Struct({
|
||||
type: Schema.tag("tool-error"),
|
||||
id: ToolCallID,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
name: Schema.String,
|
||||
message: Schema.String,
|
||||
error: Schema.optional(Schema.Defect()),
|
||||
@@ -347,14 +334,12 @@ const responseUsage = (events: ReadonlyArray<LLMEvent>) =>
|
||||
interface ContentAssembly {
|
||||
readonly contentIndex: number
|
||||
readonly text: string
|
||||
readonly itemId?: ResponseItemID
|
||||
readonly providerMetadata?: ProviderMetadata
|
||||
}
|
||||
|
||||
interface ToolInputAssembly {
|
||||
readonly name: string
|
||||
readonly text: string
|
||||
readonly itemId?: ResponseItemID
|
||||
readonly providerMetadata?: ProviderMetadata
|
||||
}
|
||||
|
||||
@@ -400,27 +385,11 @@ const appendEvent = (state: ResponseState, event: LLMEvent): ResponseState => {
|
||||
}
|
||||
}
|
||||
|
||||
const textContent = (
|
||||
text: string,
|
||||
itemId: ResponseItemID | undefined,
|
||||
providerMetadata: ProviderMetadata | undefined,
|
||||
): ContentPart => ({
|
||||
type: "text",
|
||||
text,
|
||||
...(itemId === undefined ? {} : { itemId }),
|
||||
...(providerMetadata === undefined ? {} : { providerMetadata }),
|
||||
})
|
||||
const textContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart =>
|
||||
providerMetadata === undefined ? { type: "text", text } : { type: "text", text, providerMetadata }
|
||||
|
||||
const reasoningContent = (
|
||||
text: string,
|
||||
itemId: ResponseItemID | undefined,
|
||||
providerMetadata: ProviderMetadata | undefined,
|
||||
): ContentPart => ({
|
||||
type: "reasoning",
|
||||
text,
|
||||
...(itemId === undefined ? {} : { itemId }),
|
||||
...(providerMetadata === undefined ? {} : { providerMetadata }),
|
||||
})
|
||||
const reasoningContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart =>
|
||||
providerMetadata === undefined ? { type: "reasoning", text } : { type: "reasoning", text, providerMetadata }
|
||||
|
||||
const contentWith = (state: ResponseState, content: ReadonlyArray<ContentPart>): ResponseState => ({
|
||||
...state,
|
||||
@@ -435,32 +404,26 @@ const replaceContent = (state: ResponseState, index: number, part: ContentPart)
|
||||
state.message.content.map((item, itemIndex) => (itemIndex === index ? part : item)),
|
||||
)
|
||||
|
||||
const ensureText = (
|
||||
state: ResponseState,
|
||||
id: string,
|
||||
itemId?: ResponseItemID,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
): ResponseState => {
|
||||
const ensureText = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => {
|
||||
if (state.textParts[id]) return state
|
||||
return {
|
||||
...appendContent(state, textContent("", itemId, providerMetadata)),
|
||||
...appendContent(state, textContent("", providerMetadata)),
|
||||
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 started = ensureText(state, event.id, event.itemId, event.providerMetadata)
|
||||
const started = ensureText(state, event.id, event.providerMetadata)
|
||||
const current = started.textParts[event.id]
|
||||
if (!current) return started
|
||||
const text = current.text + event.text
|
||||
const itemId = event.itemId ?? current.itemId
|
||||
const providerMetadata = event.providerMetadata ?? current.providerMetadata
|
||||
return {
|
||||
...replaceContent(started, current.contentIndex, textContent(text, itemId, providerMetadata)),
|
||||
textParts: { ...started.textParts, [event.id]: { ...current, text, itemId, providerMetadata } },
|
||||
...replaceContent(started, current.contentIndex, textContent(text, 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]
|
||||
if (!current) return state
|
||||
const providerMetadata = event.providerMetadata ?? current.providerMetadata
|
||||
const itemId = event.itemId ?? current.itemId
|
||||
return {
|
||||
...replaceContent(state, current.contentIndex, textContent(current.text, itemId, providerMetadata)),
|
||||
textParts: { ...state.textParts, [event.id]: { ...current, itemId, providerMetadata } },
|
||||
...replaceContent(state, current.contentIndex, textContent(current.text, providerMetadata)),
|
||||
textParts: { ...state.textParts, [event.id]: { ...current, providerMetadata } },
|
||||
}
|
||||
}
|
||||
|
||||
const ensureReasoning = (
|
||||
state: ResponseState,
|
||||
id: string,
|
||||
itemId?: ResponseItemID,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
): ResponseState => {
|
||||
const ensureReasoning = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => {
|
||||
if (state.reasoningParts[id]) return state
|
||||
return {
|
||||
...appendContent(state, reasoningContent("", itemId, providerMetadata)),
|
||||
...appendContent(state, reasoningContent("", providerMetadata)),
|
||||
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 started = ensureReasoning(state, event.id, event.itemId, event.providerMetadata)
|
||||
const started = ensureReasoning(state, event.id, event.providerMetadata)
|
||||
const current = started.reasoningParts[event.id]
|
||||
if (!current) return started
|
||||
const text = current.text + event.text
|
||||
const itemId = event.itemId ?? current.itemId
|
||||
const providerMetadata = event.providerMetadata ?? current.providerMetadata
|
||||
return {
|
||||
...replaceContent(started, current.contentIndex, reasoningContent(text, itemId, providerMetadata)),
|
||||
reasoningParts: { ...started.reasoningParts, [event.id]: { ...current, text, itemId, providerMetadata } },
|
||||
...replaceContent(started, current.contentIndex, reasoningContent(text, 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]
|
||||
if (!current) return state
|
||||
const providerMetadata = event.providerMetadata ?? current.providerMetadata
|
||||
const itemId = event.itemId ?? current.itemId
|
||||
return {
|
||||
...replaceContent(state, current.contentIndex, reasoningContent(current.text, itemId, providerMetadata)),
|
||||
reasoningParts: { ...state.reasoningParts, [event.id]: { ...current, itemId, providerMetadata } },
|
||||
...replaceContent(state, current.contentIndex, reasoningContent(current.text, providerMetadata)),
|
||||
reasoningParts: { ...state.reasoningParts, [event.id]: { ...current, providerMetadata } },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +474,7 @@ const reduceToolInputStart = (state: ResponseState, event: ToolInputStart): Resp
|
||||
...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]: {
|
||||
...current,
|
||||
name: event.name,
|
||||
itemId: event.itemId ?? current.itemId,
|
||||
providerMetadata: event.providerMetadata ?? current.providerMetadata,
|
||||
},
|
||||
},
|
||||
@@ -550,7 +504,6 @@ const reduceToolInputEnd = (state: ResponseState, event: ToolInputEnd): Response
|
||||
const toolCallContent = (event: ToolCall): ContentPart =>
|
||||
ToolCallPart.make({
|
||||
id: event.id,
|
||||
...(event.itemId === undefined ? {} : { itemId: event.itemId }),
|
||||
name: event.name,
|
||||
input: event.input,
|
||||
...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }),
|
||||
@@ -560,7 +513,6 @@ const toolCallContent = (event: ToolCall): ContentPart =>
|
||||
const toolResultContent = (event: ToolResult): ContentPart =>
|
||||
ToolResultPart.make({
|
||||
id: event.id,
|
||||
...(event.itemId === undefined ? {} : { itemId: event.itemId }),
|
||||
name: event.name,
|
||||
result: event.result,
|
||||
...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }),
|
||||
@@ -576,13 +528,13 @@ const reduceResponseState = (state: ResponseState, event: LLMEvent): ResponseSta
|
||||
const next = appendEvent(state, event)
|
||||
switch (event.type) {
|
||||
case "text-start":
|
||||
return ensureText(next, event.id, event.itemId, event.providerMetadata)
|
||||
return ensureText(next, event.id, event.providerMetadata)
|
||||
case "text-delta":
|
||||
return reduceTextDelta(next, event)
|
||||
case "text-end":
|
||||
return reduceTextEnd(next, event)
|
||||
case "reasoning-start":
|
||||
return ensureReasoning(next, event.id, event.itemId, event.providerMetadata)
|
||||
return ensureReasoning(next, event.id, event.providerMetadata)
|
||||
case "reasoning-delta":
|
||||
return reduceReasoningDelta(next, event)
|
||||
case "reasoning-end":
|
||||
|
||||
@@ -21,9 +21,6 @@ export type ProviderID = typeof ProviderID.Type
|
||||
export const ResponseID = Schema.String
|
||||
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 type ContentBlockID = Schema.Schema.Type<typeof ContentBlockID>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
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 { isRecord } from "../utils/record"
|
||||
|
||||
@@ -25,7 +25,6 @@ export const SystemPart = Object.assign(systemPartSchema, {
|
||||
export const TextPart = Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
cache: Schema.optional(CacheHint),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
@@ -122,7 +121,6 @@ export const ToolCallPart = Object.assign(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool-call"),
|
||||
id: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
name: Schema.String,
|
||||
input: Schema.Unknown,
|
||||
providerExecuted: Schema.optional(Schema.Boolean),
|
||||
@@ -140,7 +138,6 @@ export const ToolResultPart = Object.assign(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool-result"),
|
||||
id: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
name: Schema.String,
|
||||
result: ToolResultValue,
|
||||
providerExecuted: Schema.optional(Schema.Boolean),
|
||||
@@ -157,7 +154,6 @@ export const ToolResultPart = Object.assign(
|
||||
): ToolResultPart => ({
|
||||
type: "tool-result",
|
||||
id: input.id,
|
||||
...(input.itemId === undefined ? {} : { itemId: input.itemId }),
|
||||
name: input.name,
|
||||
result: ToolResultValue.make(input.result, input.resultType),
|
||||
providerExecuted: input.providerExecuted,
|
||||
@@ -172,7 +168,6 @@ export type ToolResultPart = Schema.Schema.Type<typeof ToolResultPart>
|
||||
export const ReasoningPart = Schema.Struct({
|
||||
type: Schema.Literal("reasoning"),
|
||||
text: Schema.String,
|
||||
itemId: Schema.optional(ResponseItemID),
|
||||
encrypted: Schema.optional(Schema.String),
|
||||
cache: Schema.optional(CacheHint),
|
||||
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 class Message extends Schema.Class<Message>("LLM.Message")({
|
||||
id: Schema.optional(ResponseItemID),
|
||||
id: Schema.optional(Schema.String),
|
||||
role: MessageRole,
|
||||
content: Schema.Array(ContentPart),
|
||||
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),
|
||||
http: Schema.optional(HttpOptions),
|
||||
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)),
|
||||
}) {}
|
||||
|
||||
@@ -294,6 +291,7 @@ export namespace LLMRequest {
|
||||
providerOptions: request.providerOptions,
|
||||
http: request.http,
|
||||
cache: request.cache,
|
||||
promptCacheKey: request.promptCacheKey,
|
||||
metadata: request.metadata,
|
||||
})
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement,
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
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,
|
||||
result: settlement.result,
|
||||
output: settlement.output,
|
||||
...(call.providerMetadata === undefined ? {} : { providerMetadata: call.providerMetadata }),
|
||||
providerMetadata: call.providerMetadata,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
+6
-11
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
+1
-1
@@ -44,7 +44,7 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\",\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}"
|
||||
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ProviderMetadata,
|
||||
type ToolCallPart,
|
||||
ToolResultPart,
|
||||
type ToolResultValue,
|
||||
type Usage,
|
||||
} from "../../src/schema"
|
||||
import { type Tools, toDefinitions } from "../../src/tool"
|
||||
@@ -60,10 +61,9 @@ export const runTools = <T extends Tools>(options: RunOptions<T>) =>
|
||||
...dispatched.map(([call, dispatched]) =>
|
||||
Message.tool({
|
||||
id: call.id,
|
||||
itemId: dispatched.events.find(LLMEvent.is.toolResult)?.itemId,
|
||||
name: call.name,
|
||||
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) {
|
||||
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") {
|
||||
appendText(
|
||||
assistantContent,
|
||||
event.type === "text-end" ? "text" : "reasoning",
|
||||
"",
|
||||
event.itemId,
|
||||
event.providerMetadata,
|
||||
)
|
||||
appendText(assistantContent, event.type === "text-end" ? "text" : "reasoning", "", event.providerMetadata)
|
||||
} else if (event.type === "tool-call") {
|
||||
assistantContent.push(event)
|
||||
if (!event.providerExecuted) toolCalls.push(event)
|
||||
@@ -105,7 +99,6 @@ const stepState = (events: ReadonlyArray<LLMEvent>) => {
|
||||
assistantContent.push(
|
||||
ToolResultPart.make({
|
||||
id: event.id,
|
||||
itemId: event.itemId,
|
||||
name: event.name,
|
||||
result: event.result,
|
||||
providerExecuted: true,
|
||||
@@ -125,7 +118,6 @@ const appendText = (
|
||||
content: ContentPart[],
|
||||
type: "text" | "reasoning",
|
||||
text: string,
|
||||
itemId?: string,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
) => {
|
||||
const last = content.at(-1)
|
||||
@@ -133,12 +125,11 @@ const appendText = (
|
||||
content[content.length - 1] = {
|
||||
...last,
|
||||
text: `${last.text}${text}`,
|
||||
itemId: itemId ?? last.itemId,
|
||||
providerMetadata: providerMetadata ?? last.providerMetadata,
|
||||
}
|
||||
return
|
||||
}
|
||||
content.push({ type, text, itemId, providerMetadata })
|
||||
content.push({ type, text, providerMetadata })
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } })
|
||||
LLM.request({ model, prompt: "Hello", promptCacheKey: "cache" })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string.
|
||||
providerOptions: { openai: { promptCacheKey: 1 } },
|
||||
// @ts-expect-error Prompt cache keys must be strings.
|
||||
promptCacheKey: 1,
|
||||
})
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "../../src"
|
||||
import * as Azure from "../../src/providers/azure"
|
||||
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 { ProviderShared } from "../../src/protocols/shared"
|
||||
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", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("Open Responses-compatible route", () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(prepared.body).toMatchObject({
|
||||
expect(prepared.body).toEqual({
|
||||
model: "example-model",
|
||||
input: [
|
||||
{ role: "system", content: "You are concise." },
|
||||
@@ -53,8 +53,6 @@ describe("Open Responses-compatible route", () => {
|
||||
],
|
||||
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,
|
||||
prompt: "Say hi.",
|
||||
generation: { maxTokens: 16, temperature: 0 },
|
||||
providerOptions: { openai: { promptCacheKey: "recorded-cache-test" } },
|
||||
promptCacheKey: "recorded-cache-test",
|
||||
})
|
||||
|
||||
const recorded = recordedTests({
|
||||
|
||||
@@ -69,9 +69,6 @@ describe("OpenAI Responses route", () => {
|
||||
stream: true,
|
||||
max_output_tokens: 20,
|
||||
temperature: 0,
|
||||
tool_choice: undefined,
|
||||
tools: undefined,
|
||||
top_p: undefined,
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -332,7 +329,7 @@ describe("OpenAI Responses route", () => {
|
||||
yield* LLMClient.generate(
|
||||
LLMRequest.update(request, {
|
||||
model: Azure.configure({
|
||||
baseURL: "https://opencode-test.openai.azure.com/openai/",
|
||||
baseURL: "https://opencode-test.openai.azure.com/openai/v1/",
|
||||
apiKey: "azure-key",
|
||||
headers: { authorization: "Bearer stale" },
|
||||
}).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",
|
||||
input: [
|
||||
{ role: "user", content: [{ type: "input_text", text: "What is the weather?" }] },
|
||||
@@ -428,60 +425,6 @@ describe("OpenAI Responses route", () => {
|
||||
tools: 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("preserves opaque assistant item ids without assigning ids to function items", () =>
|
||||
Effect.gen(function* () {
|
||||
const canonical = LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{ type: "text", text: "Calling.", itemId: "plain-text" },
|
||||
ToolCallPart.make({ id: "call_1", itemId: "plain-call", name: "lookup", input: {} }),
|
||||
]),
|
||||
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",
|
||||
"plain-call",
|
||||
])
|
||||
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", call_id: "call_1", name: "lookup", arguments: "{}" },
|
||||
{ type: "function_call_output", call_id: "call_1", output: '"done"' },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -739,9 +682,9 @@ describe("OpenAI Responses route", () => {
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
|
||||
prompt: "think",
|
||||
promptCacheKey: "session_123",
|
||||
providerOptions: {
|
||||
openai: {
|
||||
promptCacheKey: "session_123",
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
@@ -860,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* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({
|
||||
baseURL: "https://api.openai.test/v1/",
|
||||
apiKey: "test",
|
||||
providerOptions: { openai: { promptCacheKey: "model_cache" } },
|
||||
}).model("gpt-4.1-mini"),
|
||||
prompt: "no cache",
|
||||
providerOptions: { openai: { promptCacheKey: "request_cache" } },
|
||||
promptCacheKey: "request_cache",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -921,21 +863,9 @@ describe("OpenAI Responses route", () => {
|
||||
expect(response.text).toBe("Hello!")
|
||||
expect(response.events).toEqual([
|
||||
{ type: "step-start", index: 0 },
|
||||
{ type: "text-start", id: "msg_1", itemId: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
|
||||
{
|
||||
type: "text-delta",
|
||||
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-start", id: "msg_1" },
|
||||
{ type: "text-delta", id: "msg_1", text: "Hello" },
|
||||
{ type: "text-delta", id: "msg_1", text: "!" },
|
||||
{ type: "text-end", id: "msg_1" },
|
||||
{
|
||||
type: "step-finish",
|
||||
@@ -992,20 +922,17 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "text",
|
||||
text: "Checking.",
|
||||
itemId: "msg_commentary",
|
||||
providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } },
|
||||
providerMetadata: { openai: { phase: "commentary" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Finished.",
|
||||
itemId: "msg_final",
|
||||
providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } },
|
||||
providerMetadata: { openai: { phase: "final_answer" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Unclassified.",
|
||||
itemId: "msg_null",
|
||||
providerMetadata: { openai: { itemId: "msg_null", phase: null } },
|
||||
providerMetadata: { openai: { phase: null } },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1013,19 +940,16 @@ describe("OpenAI Responses route", () => {
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
id: "msg_commentary",
|
||||
content: [{ type: "output_text", text: "Checking." }],
|
||||
phase: "commentary",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
id: "msg_final",
|
||||
content: [{ type: "output_text", text: "Finished." }],
|
||||
phase: "final_answer",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
id: "msg_null",
|
||||
content: [{ type: "output_text", text: "Unclassified." }],
|
||||
phase: null,
|
||||
},
|
||||
@@ -1118,24 +1042,12 @@ describe("OpenAI Responses route", () => {
|
||||
)
|
||||
|
||||
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-delta",
|
||||
id: "msg_1",
|
||||
itemId: "msg_1",
|
||||
text: "First",
|
||||
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" } } },
|
||||
{ type: "text-start", id: "msg_1" },
|
||||
{ type: "text-delta", id: "msg_1", text: "First" },
|
||||
{ type: "text-end", id: "msg_1" },
|
||||
{ type: "text-start", id: "msg_2" },
|
||||
{ type: "text-delta", id: "msg_2", text: "Second" },
|
||||
{ type: "text-end", id: "msg_2" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1155,15 +1067,9 @@ describe("OpenAI Responses route", () => {
|
||||
expect(response.text).toBe("Hello")
|
||||
expect(response.events).toMatchObject([
|
||||
{ type: "step-start", index: 0 },
|
||||
{ type: "reasoning-start", id: "rs_1", itemId: "rs_1", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
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: "reasoning-start", id: "rs_1" },
|
||||
{ type: "reasoning-delta", id: "rs_1", text: "thinking" },
|
||||
{ type: "text-start", id: "msg_1" },
|
||||
{ type: "text-delta", id: "msg_1", text: "Hello" },
|
||||
{ type: "reasoning-end", id: "rs_1" },
|
||||
{ type: "text-end", id: "msg_1" },
|
||||
@@ -1172,8 +1078,8 @@ describe("OpenAI Responses route", () => {
|
||||
])
|
||||
expect(response.events.filter((event) => event.type === "finish")).toHaveLength(1)
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "thinking", itemId: "rs_1", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{ type: "text", text: "Hello", itemId: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
|
||||
{ type: "reasoning", text: "thinking" },
|
||||
{ type: "text", text: "Hello" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1204,7 +1110,6 @@ describe("OpenAI Responses route", () => {
|
||||
expect.objectContaining({
|
||||
type: "reasoning-end",
|
||||
id: "rs_1",
|
||||
itemId: "rs_1",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
}),
|
||||
)
|
||||
@@ -1245,34 +1150,19 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:0",
|
||||
itemId: "rs_1",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
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-delta", id: "rs_1:0", text: "First" },
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:1",
|
||||
itemId: "rs_1",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
id: "rs_1:1",
|
||||
itemId: "rs_1",
|
||||
text: "Second",
|
||||
providerMetadata: { openai: { itemId: "rs_1" } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:1", text: "Second" },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:1",
|
||||
itemId: "rs_1",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
{ type: "step-finish", index: 0, reason: { normalized: "stop", raw: undefined } },
|
||||
@@ -1310,8 +1200,8 @@ describe("OpenAI Responses route", () => {
|
||||
)
|
||||
|
||||
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:1", 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", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1359,7 +1249,7 @@ describe("OpenAI Responses route", () => {
|
||||
{ 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(
|
||||
sseEvents(
|
||||
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Parser now round-trips reasoning." },
|
||||
@@ -1406,7 +1296,6 @@ describe("OpenAI Responses route", () => {
|
||||
{ role: "assistant", content: [{ type: "output_text", text: "Before." }] },
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_1",
|
||||
encrypted_content: "encrypted-state",
|
||||
summary: [{ type: "summary_text", text: "Checked order." }],
|
||||
},
|
||||
@@ -1415,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* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
@@ -1433,14 +1322,7 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_1",
|
||||
summary: [{ type: "summary_text", text: "Checked the previous diff." }],
|
||||
encrypted_content: undefined,
|
||||
},
|
||||
])
|
||||
expect(prepared.body.input).toEqual([{ type: "item_reference", id: "rs_1" }])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1549,7 +1431,6 @@ describe("OpenAI Responses route", () => {
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_1",
|
||||
encrypted_content: "encrypted-state",
|
||||
summary: [
|
||||
{ type: "summary_text", text: "First" },
|
||||
@@ -1629,7 +1510,6 @@ describe("OpenAI Responses route", () => {
|
||||
outputTokens: 1,
|
||||
nonCachedInputTokens: 5,
|
||||
cacheReadInputTokens: undefined,
|
||||
cacheWriteInputTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
totalTokens: 6,
|
||||
providerMetadata: { openai: { input_tokens: 5, output_tokens: 1 } },
|
||||
@@ -1640,35 +1520,30 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "tool-input-start",
|
||||
id: "call_1",
|
||||
itemId: "item_1",
|
||||
name: "lookup",
|
||||
providerMetadata: { openai: { itemId: "item_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-input-delta",
|
||||
id: "call_1",
|
||||
itemId: "item_1",
|
||||
name: "lookup",
|
||||
text: '{"query"',
|
||||
},
|
||||
{
|
||||
type: "tool-input-delta",
|
||||
id: "call_1",
|
||||
itemId: "item_1",
|
||||
name: "lookup",
|
||||
text: ':"weather"}',
|
||||
},
|
||||
{
|
||||
type: "tool-input-end",
|
||||
id: "call_1",
|
||||
itemId: "item_1",
|
||||
name: "lookup",
|
||||
providerMetadata: { openai: { itemId: "item_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "call_1",
|
||||
itemId: "item_1",
|
||||
name: "lookup",
|
||||
input: { query: "weather" },
|
||||
providerExecuted: undefined,
|
||||
@@ -1688,17 +1563,6 @@ describe("OpenAI Responses route", () => {
|
||||
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" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1731,7 +1595,6 @@ describe("OpenAI Responses route", () => {
|
||||
expect(response.events.find(LLMEvent.is.toolInputError)).toEqual({
|
||||
type: "tool-input-error",
|
||||
id: "call_1",
|
||||
itemId: "item_1",
|
||||
name: "lookup",
|
||||
raw: '{"query":"partial',
|
||||
})
|
||||
@@ -1788,7 +1651,6 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "ws_1",
|
||||
itemId: "ws_1",
|
||||
name: "web_search",
|
||||
input: { type: "search", query: "effect 4" },
|
||||
providerExecuted: true,
|
||||
@@ -1797,35 +1659,11 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
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 } },
|
||||
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" } },
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1903,7 +1741,6 @@ describe("OpenAI Responses route", () => {
|
||||
expect(toolCall).toEqual({
|
||||
type: "tool-call",
|
||||
id: "ci_1",
|
||||
itemId: "ci_1",
|
||||
name: "code_interpreter",
|
||||
input: { code: "print(1+1)", container_id: "cnt_xyz" },
|
||||
providerExecuted: true,
|
||||
@@ -1913,12 +1750,10 @@ describe("OpenAI Responses route", () => {
|
||||
expect(toolResult).toEqual({
|
||||
type: "tool-result",
|
||||
id: "ci_1",
|
||||
itemId: "ci_1",
|
||||
name: "code_interpreter",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { openai: { itemId: "ci_1", item } },
|
||||
output: undefined,
|
||||
providerMetadata: { openai: { itemId: "ci_1" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -162,7 +162,6 @@ describe("OpenRouter", () => {
|
||||
openrouter: {
|
||||
usage: true,
|
||||
reasoning: { effort: "high" },
|
||||
promptCacheKey: "session_123",
|
||||
models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"],
|
||||
provider: { order: ["anthropic", "google"], require_parameters: true },
|
||||
plugins: [{ id: "response-healing" }],
|
||||
@@ -174,6 +173,7 @@ describe("OpenRouter", () => {
|
||||
},
|
||||
}).model("anthropic/claude-3.7-sonnet:thinking"),
|
||||
prompt: "Think briefly.",
|
||||
promptCacheKey: "session_123",
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -49,43 +49,6 @@ describe("LLMResponse reducer", () => {
|
||||
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", () => {
|
||||
const state = reduce([
|
||||
LLMEvent.textStart({ id: "t1" }),
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("LLMClient tools", () => {
|
||||
expect(calls).toEqual([{ id: "call_projected", parameters: { prefix: "count" }, output: { 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.events).toMatchObject([
|
||||
expect(dispatched.events).toEqual([
|
||||
LLMEvent.toolResult({
|
||||
id: "call_projected",
|
||||
name: "projected",
|
||||
@@ -180,7 +180,6 @@ describe("LLMClient tools", () => {
|
||||
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 }),
|
||||
)
|
||||
|
||||
expect(dispatched.events).toMatchObject([
|
||||
expect(dispatched.events).toEqual([
|
||||
LLMEvent.toolResult({
|
||||
id: "call_1",
|
||||
name: "tool",
|
||||
@@ -207,13 +206,12 @@ describe("LLMClient tools", () => {
|
||||
providerMetadata,
|
||||
}),
|
||||
])
|
||||
expect(dispatched.events[0]?.itemId).toBeUndefined()
|
||||
|
||||
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({
|
||||
id: "call_2",
|
||||
name: "missing",
|
||||
@@ -227,27 +225,6 @@ describe("LLMClient tools", () => {
|
||||
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.events).toMatchObject([
|
||||
expect(dispatched.events).toEqual([
|
||||
LLMEvent.toolResult({
|
||||
id: "call_1",
|
||||
name: "eventful",
|
||||
@@ -468,7 +445,6 @@ describe("LLMClient tools", () => {
|
||||
output: { structured: { ok: true }, content: [] },
|
||||
}),
|
||||
])
|
||||
expect(dispatched.events[0]?.itemId).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
}
|
||||
},
|
||||
"include": ["src", "package.json"],
|
||||
"exclude": ["dist", "ts-dist"]
|
||||
"exclude": ["dist", "ts-dist"],
|
||||
"references": [{ "path": "../core" }]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||
"benchmark:location": "bun run script/benchmark-location.ts",
|
||||
"test": "bun test --only-failures",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsgo -b tsconfig.json tsconfig.tests.json"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
|
||||
@@ -132,14 +132,16 @@ function renderMigration(name: string, sql: string) {
|
||||
return `import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: ${JSON.stringify(name)},
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
${renderStatements(sql)}
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
`
|
||||
}
|
||||
|
||||
@@ -147,13 +149,15 @@ function renderSchema(sql: string) {
|
||||
return `import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export default {
|
||||
const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
${renderStatements(sql)}
|
||||
})
|
||||
},
|
||||
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||
}
|
||||
|
||||
export default schema
|
||||
`
|
||||
}
|
||||
|
||||
@@ -191,10 +195,10 @@ async function formatTypescript(input: string) {
|
||||
function renderRegistry(names: string[]) {
|
||||
return `import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations = (
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
await Promise.all([
|
||||
${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
).map((module) => module.default)
|
||||
`
|
||||
}
|
||||
|
||||
@@ -263,6 +263,7 @@ function mapOpenRouterOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
"extraBody",
|
||||
"fetch",
|
||||
"headers",
|
||||
"promptCacheKey",
|
||||
"timeout",
|
||||
].includes(key),
|
||||
),
|
||||
@@ -279,7 +280,6 @@ function mapXAIOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
const options = {
|
||||
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
|
||||
...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
|
||||
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
|
||||
}
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: { xai: options } }
|
||||
|
||||
@@ -126,7 +126,7 @@ ${render(current)}`
|
||||
const key = Instructions.Key.make("core/codemode")
|
||||
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)
|
||||
return Instructions.make({
|
||||
key,
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations = (
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
await Promise.all([
|
||||
import("./migration/20260127222353_familiar_lady_ursula"),
|
||||
import("./migration/20260211171708_add_project_commands"),
|
||||
@@ -43,4 +43,4 @@ export const migrations = (
|
||||
import("./migration/20260804233008_loose_psylocke"),
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260127222353_familiar_lady_ursula",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -104,4 +104,6 @@ export default {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260211171708_add_project_commands",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260213144116_wakeful_the_professor",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260225215848_workspace",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260227213759_add_session_workspace_id",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -9,4 +9,6 @@ export default {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260228203230_blue_harpoon",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260303231226_add_workspace_fields",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,4 +12,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260309230000_move_org_to_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,4 +12,6 @@ export default {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260312043431_session_message_cursor",
|
||||
up(tx) {
|
||||
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\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260323234822_events",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260410174513_workspace-name",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -26,4 +26,6 @@ export default {
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260413175956_chief_energizer",
|
||||
up(tx) {
|
||||
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\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260423070820_add_icon_url_override",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260427172553_slow_nightmare",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -27,4 +27,6 @@ export default {
|
||||
yield* tx.run(`DROP TABLE \`session_entry\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260428004200_add_session_path",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260501142318_next_venus",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -9,4 +9,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260504145000_add_sync_owner",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260507164347_add_workspace_time",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260510033149_session_usage",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260511000411_data_migration_state",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260511173437_session-metadata",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -13,4 +13,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260601010001_normalize_storage_paths",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260601202201_amazing_prowler",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DROP TABLE \`permission\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260602002951_lowly_union_jack",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260602182828_add_project_directories",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -17,4 +17,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603001617_session_message_projection_indexes",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -16,4 +16,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603040000_session_message_projection_order",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603141458_session_input_inbox",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603160727_jittery_ezekiel_stane",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260604172448_event_sourced_session_input",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260605003541_add_session_context_snapshot",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260605042240_add_context_epoch_agent",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260611035744_credential",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260611192811_lush_chimera",
|
||||
up(tx) {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260612174303_project_dir_strategy",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -26,4 +26,6 @@ export default {
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260622142730_simplify_session_context_epoch",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -10,4 +10,6 @@ export default {
|
||||
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 type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260622170816_reset_v2_session_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,4 +12,6 @@ export default {
|
||||
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260622202450_simplify_session_input",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -14,4 +14,6 @@ export default {
|
||||
yield* tx.run(`DELETE FROM \`workspace\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260804233008_loose_psylocke",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -135,4 +135,6 @@ export default {
|
||||
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 wellKnownSourcesKey = "wellknown:sources"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260805200742_import_legacy_credentials",
|
||||
up(tx) {
|
||||
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) {
|
||||
return Effect.gen(function* () {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export default {
|
||||
const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
@@ -248,4 +248,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||
}
|
||||
|
||||
export default schema
|
||||
|
||||
@@ -18,7 +18,7 @@ const Files = Schema.Array(File)
|
||||
const key = Instructions.Key.make("core/instructions")
|
||||
|
||||
export interface Interface {
|
||||
readonly load: () => Effect.Effect<Instructions.Instructions>
|
||||
readonly load: () => Effect.Effect<Instructions.List>
|
||||
}
|
||||
|
||||
export const Options = Schema.Struct({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SessionSchema } from "../session/schema"
|
||||
import { Instructions } from "./index"
|
||||
|
||||
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") {}
|
||||
|
||||
@@ -53,7 +53,7 @@ export declare namespace Source {
|
||||
}
|
||||
|
||||
/** Ordered sources; identical values render identical bytes. */
|
||||
export type Instructions = ReadonlyArray<Source>
|
||||
export type List = ReadonlyArray<Source>
|
||||
|
||||
export type ReadResult = ReadonlyArray<{
|
||||
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. */
|
||||
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 encode = Schema.encodeSync(source.codec)
|
||||
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 keys = new Set<Key>()
|
||||
for (const source of sources) {
|
||||
@@ -131,7 +131,7 @@ export function combine(values: ReadonlyArray<Instructions>): Instructions {
|
||||
return sources
|
||||
}
|
||||
|
||||
export function read(value: Instructions): Effect.Effect<ReadResult> {
|
||||
export function read(value: List): Effect.Effect<ReadResult> {
|
||||
return Effect.forEach(
|
||||
value,
|
||||
(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 })
|
||||
}
|
||||
|
||||
export function renderInitial(value: Instructions, values: Readonly<Record<string, Schema.Json>>) {
|
||||
export function renderInitial(value: List, values: Readonly<Record<string, Schema.Json>>) {
|
||||
return render(
|
||||
value.flatMap((source) => {
|
||||
if (!Object.hasOwn(values, source.key)) return []
|
||||
@@ -169,7 +169,7 @@ export function renderInitial(value: Instructions, values: Readonly<Record<strin
|
||||
}
|
||||
|
||||
export function renderUpdate(
|
||||
value: Instructions,
|
||||
value: List,
|
||||
previous: Readonly<Record<string, Schema.Json>>,
|
||||
delta: Readonly<Record<string, Option.Option<Schema.Json>>>,
|
||||
) {
|
||||
|
||||
@@ -55,7 +55,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
|
||||
}
|
||||
|
||||
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") {}
|
||||
|
||||
@@ -54,7 +54,7 @@ const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyA
|
||||
}
|
||||
|
||||
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") {}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { llmClient } from "../effect/app-node-platform"
|
||||
import { SessionEvent } from "./event"
|
||||
import type { SessionMessage } from "./message"
|
||||
import { SessionModelHeaders } from "./model-headers"
|
||||
import { SessionPromptCacheKey } from "./prompt-cache-key"
|
||||
import { App } from "../app"
|
||||
import { SessionRunnerModel } from "./runner/model"
|
||||
import { SessionSchema } from "./schema"
|
||||
@@ -258,6 +259,7 @@ const make = (dependencies: Dependencies) => {
|
||||
.stream(
|
||||
LLM.request({
|
||||
model: plan.model,
|
||||
promptCacheKey: SessionPromptCacheKey.make(plan.session.id),
|
||||
http: { headers: SessionModelHeaders.make(plan.session, dependencies.app) },
|
||||
messages: [Message.user(plan.prompt)],
|
||||
tools: [],
|
||||
|
||||
@@ -25,7 +25,7 @@ import { SessionStore } from "./store"
|
||||
export interface Selection {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly agent: Agent.Selection & { readonly info: Agent.Info }
|
||||
readonly instructions: Instructions.Instructions
|
||||
readonly instructions: Instructions.List
|
||||
readonly tools: Tool.Snapshot
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SessionContext } from "./context"
|
||||
import { SessionGenerate } from "./generate"
|
||||
import { SessionHistory } from "./history"
|
||||
import { SessionModelHeaders } from "./model-headers"
|
||||
import { SessionPromptCacheKey } from "./prompt-cache-key"
|
||||
import { SessionRunnerModel } from "./runner/model"
|
||||
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
|
||||
import { toLLMMessages } from "./runner/to-llm-message"
|
||||
@@ -31,9 +32,6 @@ export const layer = Layer.effect(
|
||||
const model = yield* models.resolve(selection.session)
|
||||
const history = yield* SessionHistory.preview(database.db, selection.session.id, selection.instructions)
|
||||
const providerMetadataKey = model.model.route.providerMetadataKey ?? model.model.provider
|
||||
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id)
|
||||
? selection.session.id.slice(4)
|
||||
: selection.session.id
|
||||
const tools = selection.tools
|
||||
const toolDefinitions = tools.definitions
|
||||
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
|
||||
@@ -71,7 +69,7 @@ export const layer = Layer.effect(
|
||||
LLM.request({
|
||||
model: model.model,
|
||||
http: { headers: SessionModelHeaders.make(selection.session, app) },
|
||||
providerOptions: { [providerMetadataKey]: { promptCacheKey } },
|
||||
promptCacheKey: SessionPromptCacheKey.make(selection.session.id),
|
||||
system: contextEvent.system,
|
||||
messages: contextEvent.messages,
|
||||
tools: hookedTools,
|
||||
|
||||
@@ -74,7 +74,7 @@ export const load = Effect.fn("SessionHistory.load")(function* (db: DatabaseServ
|
||||
export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
) {
|
||||
return yield* db
|
||||
.transaction(() =>
|
||||
@@ -92,7 +92,7 @@ export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(fun
|
||||
export const preview = Effect.fn("SessionHistory.preview")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
) {
|
||||
const observed = yield* Instructions.read(instructions)
|
||||
return yield* db
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface Interface {
|
||||
}) => Effect.Effect<void, InstructionEntry.ValueTooLargeError>
|
||||
readonly remove: (input: { readonly sessionID: SessionSchema.ID; readonly key: Key }) => Effect.Effect<void>
|
||||
/** 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") {}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface Observation extends Instructions.Admission {
|
||||
|
||||
export const observe = Effect.fn("InstructionState.observe")(function* (
|
||||
db: DatabaseService,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
sessionID: SessionSchema.ID,
|
||||
): Effect.fn.Return<Observation, Instructions.InitializationBlocked> {
|
||||
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* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
observation: Observation,
|
||||
) {
|
||||
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* (
|
||||
db: DatabaseService,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
observation: Observation,
|
||||
) {
|
||||
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* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
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* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
) {
|
||||
const state = yield* find(db, 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* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
instructions: Instructions.Instructions,
|
||||
instructions: Instructions.List,
|
||||
observed: Instructions.ReadResult,
|
||||
) {
|
||||
const state = yield* find(db, sessionID)
|
||||
|
||||
@@ -15,6 +15,7 @@ import { QuestionTool } from "../tool/plugin/question"
|
||||
import { Tool } from "../tool"
|
||||
import { SessionContext } from "./context"
|
||||
import { SessionModelHeaders } from "./model-headers"
|
||||
import { SessionPromptCacheKey } from "./prompt-cache-key"
|
||||
import { PromptCacheDiagnostics } from "./prompt-cache-diagnostics"
|
||||
import { MAX_STEPS_PROMPT } from "./runner/max-steps"
|
||||
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
|
||||
@@ -181,7 +182,6 @@ export const layer = Layer.effect(
|
||||
// The final Step keeps definitions available to protocols with native "none",
|
||||
// preserving their prompt cache prefix. Calls are still rejected at execution.
|
||||
const tools = input.context.tools
|
||||
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
||||
const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial]
|
||||
.filter((part) => part.length > 0)
|
||||
.map(SystemPart.make)
|
||||
@@ -220,7 +220,7 @@ export const layer = Layer.effect(
|
||||
http: {
|
||||
headers: SessionModelHeaders.make(session, app),
|
||||
},
|
||||
providerOptions: { [providerMetadataKey]: { promptCacheKey } },
|
||||
promptCacheKey: SessionPromptCacheKey.make(session.id),
|
||||
system: context.system,
|
||||
messages: boundImages(unsupportedParts(context.messages, resolved.capabilities)),
|
||||
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
|
||||
@@ -116,11 +116,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
})
|
||||
const currentAssistantMessageID = () =>
|
||||
stepStarted ? Effect.succeed(assistantMessageID) : Effect.die(new Error("Tool event before assistant step start"))
|
||||
const providerState = (metadata: ProviderMetadata | undefined, itemId?: string) => {
|
||||
const state = metadata?.[input.providerMetadataKey]
|
||||
if (itemId === undefined) return state
|
||||
return { ...(typeof state === "object" && state !== null && !Array.isArray(state) ? state : {}), itemId }
|
||||
}
|
||||
const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.providerMetadataKey]
|
||||
const fragments = (
|
||||
name: string,
|
||||
ended: (id: string, value: string, ordinal: number, state?: Record<string, unknown>) => Effect.Effect<void>,
|
||||
@@ -344,7 +340,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
return
|
||||
case "text-start":
|
||||
outputStarted = true
|
||||
const startedTextOrdinal = yield* text.start(event.id, providerState(event.providerMetadata, event.itemId))
|
||||
const startedTextOrdinal = yield* text.start(event.id, providerState(event.providerMetadata))
|
||||
yield* bus.publish(SessionEvent.Text.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
@@ -352,11 +348,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
})
|
||||
return
|
||||
case "text-delta":
|
||||
const deltaTextOrdinal = yield* text.append(
|
||||
event.id,
|
||||
event.text,
|
||||
providerState(event.providerMetadata, event.itemId),
|
||||
)
|
||||
const deltaTextOrdinal = yield* text.append(event.id, event.text, providerState(event.providerMetadata))
|
||||
yield* bus.publish(SessionEvent.Text.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
@@ -365,26 +357,23 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
})
|
||||
return
|
||||
case "text-end":
|
||||
yield* text.end(event.id, providerState(event.providerMetadata, event.itemId))
|
||||
yield* text.end(event.id, providerState(event.providerMetadata))
|
||||
return
|
||||
case "reasoning-start":
|
||||
outputStarted = true
|
||||
const startedReasoningOrdinal = yield* reasoning.start(
|
||||
event.id,
|
||||
providerState(event.providerMetadata, event.itemId),
|
||||
)
|
||||
const startedReasoningOrdinal = yield* reasoning.start(event.id, providerState(event.providerMetadata))
|
||||
yield* bus.publish(SessionEvent.Reasoning.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
ordinal: startedReasoningOrdinal,
|
||||
state: providerState(event.providerMetadata, event.itemId),
|
||||
state: providerState(event.providerMetadata),
|
||||
})
|
||||
return
|
||||
case "reasoning-delta":
|
||||
const deltaReasoningOrdinal = yield* reasoning.append(
|
||||
event.id,
|
||||
event.text,
|
||||
providerState(event.providerMetadata, event.itemId),
|
||||
providerState(event.providerMetadata),
|
||||
)
|
||||
yield* bus.publish(SessionEvent.Reasoning.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
@@ -394,7 +383,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
})
|
||||
return
|
||||
case "reasoning-end":
|
||||
yield* reasoning.end(event.id, providerState(event.providerMetadata, event.itemId))
|
||||
yield* reasoning.end(event.id, providerState(event.providerMetadata))
|
||||
return
|
||||
case "tool-input-start":
|
||||
outputStarted = true
|
||||
@@ -438,7 +427,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
id: event.id,
|
||||
input: asRecord(event.input),
|
||||
executed: tool.providerExecuted,
|
||||
state: providerState(event.providerMetadata, event.itemId),
|
||||
state: providerState(event.providerMetadata),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -456,7 +445,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
}
|
||||
tool.settled = true
|
||||
const executed = event.providerExecuted === true || tool.providerExecuted
|
||||
const resultState = providerState(event.providerMetadata, event.itemId)
|
||||
const resultState = providerState(event.providerMetadata)
|
||||
if (event.result.type === "error") {
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
@@ -496,7 +485,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
: { type: "tool.execution", message: event.message },
|
||||
...failureSnapshot(tool),
|
||||
executed: tool.providerExecuted,
|
||||
resultState: providerState(event.providerMetadata, event.itemId),
|
||||
resultState: providerState(event.providerMetadata),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -521,7 +510,8 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
|
||||
const progress = Effect.fnUntraced(function* (id: string, update: Tool.Metadata) {
|
||||
const tool = tools.get(id)
|
||||
if (!tool?.called || tool.settled) return yield* Effect.die(new Error(`Tool progress outside running call: ${id}`))
|
||||
if (!tool?.called || tool.settled)
|
||||
return yield* Effect.die(new Error(`Tool progress outside running call: ${id}`))
|
||||
tool.progress = update
|
||||
yield* bus.publish(SessionEvent.Tool.Progress, {
|
||||
sessionID: input.sessionID,
|
||||
@@ -532,7 +522,11 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
})
|
||||
|
||||
/** Publishes one canonical terminal event for a locally executed tool call. */
|
||||
const toolExecution = Effect.fnUntraced(function* (id: string, name: string, result: Tool.Result) {
|
||||
const toolExecution = Effect.fnUntraced(function* (
|
||||
id: string,
|
||||
name: string,
|
||||
result: Tool.Result,
|
||||
) {
|
||||
const tool = tools.get(id)
|
||||
if (!tool?.called) return yield* Effect.die(new Error(`Tool execution before call: ${id}`))
|
||||
if (tool.name !== name)
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Message,
|
||||
ToolCallPart,
|
||||
ToolResultPart,
|
||||
type ContentPart,
|
||||
type ProviderMetadata,
|
||||
} from "@opencode-ai/ai"
|
||||
import { Message, ToolCallPart, ToolResultPart, type ContentPart, type ProviderMetadata } from "@opencode-ai/ai"
|
||||
import { Option, Schema } from "effect"
|
||||
import type { Model } from "../../model"
|
||||
import { SessionMessage } from "../message"
|
||||
@@ -72,46 +66,27 @@ const providerMetadata = (
|
||||
state: Record<string, unknown> | undefined,
|
||||
): ProviderMetadata | undefined => (state === undefined ? undefined : { [provider]: state })
|
||||
|
||||
const responseItemID = (state: Record<string, unknown> | undefined) =>
|
||||
typeof state?.itemId === "string" ? state.itemId : undefined
|
||||
|
||||
const portableProviderState = (state: Record<string, unknown> | undefined) => {
|
||||
if (state === undefined || !("itemId" in state)) return state
|
||||
const { itemId: _itemId, ...portable } = state
|
||||
return portable
|
||||
}
|
||||
|
||||
const toolInput = (tool: SessionMessage.AssistantTool) =>
|
||||
tool.state.status === "streaming"
|
||||
? Option.getOrElse(decodeToolInput(tool.state.input), () => tool.state.input)
|
||||
: tool.state.input
|
||||
|
||||
const toolCall = (
|
||||
tool: SessionMessage.AssistantTool,
|
||||
itemId: string | undefined,
|
||||
providerMetadata: ProviderMetadata | undefined,
|
||||
): ContentPart =>
|
||||
const toolCall = (tool: SessionMessage.AssistantTool, providerMetadata: ProviderMetadata | undefined): ContentPart =>
|
||||
ToolCallPart.make({
|
||||
id: tool.id,
|
||||
...(itemId === undefined ? {} : { itemId }),
|
||||
name: tool.name,
|
||||
input: toolInput(tool),
|
||||
providerExecuted: tool.executed,
|
||||
providerMetadata,
|
||||
})
|
||||
|
||||
const toolResult = (
|
||||
tool: SessionMessage.AssistantTool,
|
||||
itemId: string | undefined,
|
||||
providerMetadata: ProviderMetadata | undefined,
|
||||
) => {
|
||||
const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: ProviderMetadata | undefined) => {
|
||||
if (tool.state.status === "completed") {
|
||||
// TODO: Materialize remote and managed URIs before provider-history lowering.
|
||||
const content = tool.state.content
|
||||
const single = content.length === 1 ? content[0] : undefined
|
||||
return ToolResultPart.make({
|
||||
id: tool.id,
|
||||
...(itemId === undefined ? {} : { itemId }),
|
||||
name: tool.name,
|
||||
result:
|
||||
single?.type === "text"
|
||||
@@ -124,7 +99,6 @@ const toolResult = (
|
||||
if (tool.state.status === "error") {
|
||||
return ToolResultPart.make({
|
||||
id: tool.id,
|
||||
...(itemId === undefined ? {} : { itemId }),
|
||||
name: tool.name,
|
||||
result: { error: tool.state.error, content: tool.state.content ?? [] },
|
||||
resultType: "error",
|
||||
@@ -144,13 +118,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
|
||||
{
|
||||
type: "text",
|
||||
text: item.text,
|
||||
itemId: reuseProviderMetadata ? responseItemID(item.state) : undefined,
|
||||
providerMetadata: sameProvider
|
||||
? providerMetadata(
|
||||
providerMetadataKey,
|
||||
reuseProviderMetadata ? item.state : portableProviderState(item.state),
|
||||
)
|
||||
: undefined,
|
||||
providerMetadata: sameProvider ? providerMetadata(providerMetadataKey, item.state) : undefined,
|
||||
},
|
||||
]
|
||||
if (item.type === "reasoning")
|
||||
@@ -159,7 +127,6 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
|
||||
{
|
||||
type: "reasoning",
|
||||
text: item.text,
|
||||
itemId: responseItemID(item.state),
|
||||
providerMetadata: providerMetadata(providerMetadataKey, item.state),
|
||||
},
|
||||
]
|
||||
@@ -171,7 +138,6 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
|
||||
(sameModel && item.executed === true && (item.state.status === "completed" || item.state.status === "error"))
|
||||
const call = toolCall(
|
||||
item,
|
||||
reuseToolProviderMetadata ? responseItemID(item.providerState) : undefined,
|
||||
reuseToolProviderMetadata ? providerMetadata(providerMetadataKey, item.providerState) : undefined,
|
||||
)
|
||||
if (item.executed !== true) return [call]
|
||||
@@ -179,11 +145,6 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
|
||||
// replay must survive a model switch within the same provider.
|
||||
const result = toolResult(
|
||||
item,
|
||||
reuseToolProviderMetadata
|
||||
? responseItemID(item.providerResultState ?? (item.executed === true ? item.providerState : undefined))
|
||||
: sameProvider && item.executed === true
|
||||
? responseItemID(item.providerResultState)
|
||||
: undefined,
|
||||
reuseToolProviderMetadata
|
||||
? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState)
|
||||
: sameProvider && item.executed === true && item.providerResultState !== undefined
|
||||
@@ -202,8 +163,9 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
|
||||
.map((item) =>
|
||||
toolResult(
|
||||
item,
|
||||
responseItemID(item.providerResultState) ?? `fco_${item.id}`,
|
||||
reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.providerResultState) : undefined,
|
||||
reuseProviderMetadata
|
||||
? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState)
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
.filter((message) => message !== undefined)
|
||||
@@ -242,7 +204,7 @@ function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMe
|
||||
case "skill":
|
||||
return [Message.make({ id: message.id, role: "user", content: message.text, metadata: message.metadata })]
|
||||
case "system":
|
||||
return [Message.make({ id: message.id, role: "system", content: message.text })]
|
||||
return [Message.system(message.text)]
|
||||
case "shell":
|
||||
return [
|
||||
Message.make({
|
||||
|
||||
@@ -57,7 +57,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
|
||||
}
|
||||
|
||||
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") {}
|
||||
|
||||
@@ -22,7 +22,7 @@ export abstract class NamedError extends Error {
|
||||
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({
|
||||
name: Schema.Literal(name),
|
||||
data,
|
||||
|
||||
@@ -179,7 +179,6 @@ describe("AISDKNative", () => {
|
||||
models: ["anthropic/claude-sonnet-4.6"],
|
||||
provider: { only: ["anthropic"], require_parameters: true },
|
||||
reasoning: { effort: "high" },
|
||||
promptCacheKey: "session_123",
|
||||
future_option: { enabled: true },
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -190,7 +189,6 @@ describe("AISDKNative", () => {
|
||||
models: ["anthropic/claude-sonnet-4.6"],
|
||||
provider: { only: ["anthropic"], require_parameters: true },
|
||||
reasoning: { effort: "high" },
|
||||
promptCacheKey: "session_123",
|
||||
future_option: { enabled: true },
|
||||
},
|
||||
},
|
||||
@@ -271,7 +269,6 @@ describe("AISDKNative", () => {
|
||||
baseURL: "https://xai.example/v1",
|
||||
reasoningEffort: "custom",
|
||||
store: true,
|
||||
promptCacheKey: "cache-key",
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode-ai/ai/providers/xai",
|
||||
@@ -282,7 +279,6 @@ describe("AISDKNative", () => {
|
||||
xai: {
|
||||
reasoningEffort: "custom",
|
||||
store: true,
|
||||
promptCacheKey: "cache-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -68,7 +68,7 @@ const instructionEvents = (db: Database.Interface["db"], sessionID: SessionSchem
|
||||
.all()
|
||||
.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(
|
||||
Effect.flatMap((observed) => InstructionState.preview(db, sessionID, instructions, observed)),
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ export const state = (values: Readonly<Record<string, Schema.Json>>): State => (
|
||||
const hashes = (values: Readonly<Record<string, Schema.Json>>): Instructions.Values =>
|
||||
Object.fromEntries(Object.entries(values).map(([key, value]) => [key, Instructions.hash(value)]))
|
||||
|
||||
export const readInitial = (instructions: Instructions.Instructions) =>
|
||||
export const readInitial = (instructions: Instructions.List) =>
|
||||
Effect.gen(function* () {
|
||||
const admission = yield* Instructions.read(instructions).pipe(Effect.flatMap(Instructions.diff))
|
||||
const current = state(
|
||||
@@ -23,7 +23,7 @@ export const readInitial = (instructions: Instructions.Instructions) =>
|
||||
return { ...current, text: Instructions.renderInitial(instructions, current.values) }
|
||||
})
|
||||
|
||||
export const readUpdate = (instructions: Instructions.Instructions, previous: State) =>
|
||||
export const readUpdate = (instructions: Instructions.List, previous: State) =>
|
||||
Effect.gen(function* () {
|
||||
const admission = yield* Instructions.read(instructions).pipe(
|
||||
Effect.flatMap((observed) => Instructions.diff(observed, hashes(previous.values))),
|
||||
|
||||
@@ -236,6 +236,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
expect(Array.from(yield* Fiber.join(delta)).map((event) => event.data.text)).toEqual(["manual summary"])
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(requests[0]?.promptCacheKey).toBe(sessionID)
|
||||
expect(requests[0]?.http?.headers).toEqual({
|
||||
"x-session-affinity": sessionID,
|
||||
"X-Session-Id": sessionID,
|
||||
|
||||
@@ -296,7 +296,7 @@ it.effect("generates from fresh settled Session context without durable mutation
|
||||
expect(requests[0]?.system[0]?.text).toBe("Hooked system")
|
||||
expect(requests[0]?.system.map((part) => part.text)).toContain("Initial context")
|
||||
expect(requests[0]?.http?.headers).toMatchObject({ "X-Session-Id": sessionID })
|
||||
expect(requests[0]?.providerOptions).toMatchObject({ openai: { promptCacheKey: sessionID } })
|
||||
expect(requests[0]?.promptCacheKey).toBe(sessionID)
|
||||
const instructionUpdates = requests[0]?.messages.flatMap((message) =>
|
||||
message.role === "system"
|
||||
? message.content.flatMap((content) => (content.type === "text" ? [content.text] : []))
|
||||
|
||||
@@ -110,13 +110,7 @@ describe("toLLMMessages", () => {
|
||||
)
|
||||
|
||||
expect(messages.map((message) => message.role)).toEqual(["system", "user", "user", "user", "user"])
|
||||
expect(messages[0]).toEqual(
|
||||
Message.make({
|
||||
id: "msg_system",
|
||||
role: "system",
|
||||
content: [{ type: "text", text: "Updated context\n\nOther context" }],
|
||||
}),
|
||||
)
|
||||
expect(messages[0]).toEqual(Message.system("Updated context\n\nOther context"))
|
||||
expect(messages[1]).toEqual(
|
||||
Message.make({
|
||||
id: id("user"),
|
||||
@@ -482,7 +476,6 @@ Recent work
|
||||
{
|
||||
type: "tool-result",
|
||||
id: "completed",
|
||||
itemId: "fco_completed",
|
||||
name: "read",
|
||||
result: {
|
||||
type: "content",
|
||||
@@ -520,7 +513,6 @@ Recent work
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Think",
|
||||
itemId: "rs_1",
|
||||
providerMetadata: { provider: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
])
|
||||
@@ -610,7 +602,6 @@ Recent work
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "hosted-completed",
|
||||
itemId: "call_completed",
|
||||
name: "web_search",
|
||||
input: { query: "Effect" },
|
||||
providerExecuted: true,
|
||||
@@ -619,7 +610,6 @@ Recent work
|
||||
{
|
||||
type: "tool-result",
|
||||
id: "hosted-completed",
|
||||
itemId: "result_completed",
|
||||
name: "web_search",
|
||||
result: { type: "text", value: '{"found":true}' },
|
||||
providerExecuted: true,
|
||||
@@ -630,7 +620,6 @@ Recent work
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "hosted-failed",
|
||||
itemId: "call_failed",
|
||||
name: "web_search",
|
||||
input: { query: "Effect" },
|
||||
providerExecuted: true,
|
||||
@@ -639,7 +628,6 @@ Recent work
|
||||
{
|
||||
type: "tool-result",
|
||||
id: "hosted-failed",
|
||||
itemId: "result_failed",
|
||||
name: "web_search",
|
||||
result: {
|
||||
type: "error",
|
||||
@@ -710,7 +698,6 @@ Recent work
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "hosted-old-model",
|
||||
itemId: undefined,
|
||||
name: "web_search",
|
||||
input: { query: "Effect" },
|
||||
providerExecuted: true,
|
||||
@@ -719,7 +706,6 @@ Recent work
|
||||
{
|
||||
type: "tool-result",
|
||||
id: "hosted-old-model",
|
||||
itemId: "hosted-old-model",
|
||||
name: "web_search",
|
||||
result: { type: "text", value: '{"status":"completed"}' },
|
||||
providerExecuted: true,
|
||||
@@ -732,7 +718,6 @@ Recent work
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "local-old-model",
|
||||
itemId: undefined,
|
||||
name: "read",
|
||||
input: { path: "README.md" },
|
||||
providerExecuted: false,
|
||||
@@ -743,7 +728,6 @@ Recent work
|
||||
{
|
||||
type: "tool-result",
|
||||
id: "local-old-model",
|
||||
itemId: "fco_local-old-model",
|
||||
name: "read",
|
||||
result: { type: "text", value: "Hello" },
|
||||
providerExecuted: false,
|
||||
|
||||
@@ -24,7 +24,9 @@ const capture = (providerMetadataKey = "anthropic", options?: { readonly interru
|
||||
const publish = Effect.sync(() => {
|
||||
const event = { id: Event.ID.create(), type: definition.type, data } as Event.Payload<typeof definition>
|
||||
published.push({
|
||||
type: definition.durable ? Bus.versionedType(definition.type, definition.durable.version) : definition.type,
|
||||
type: definition.durable
|
||||
? Bus.versionedType(definition.type, definition.durable.version)
|
||||
: definition.type,
|
||||
data,
|
||||
})
|
||||
return event
|
||||
@@ -64,10 +66,9 @@ const hostedResult = LLMEvent.toolResult({
|
||||
|
||||
test("local tool success serializes media base64 once through canonical content", async () => {
|
||||
const { published, publisher } = capture()
|
||||
const localCall = LLMEvent.toolCall({ ...call, itemId: "fc_call-image" })
|
||||
await Effect.runPromise(publisher.publish(localCall))
|
||||
await Effect.runPromise(publisher.publish(call))
|
||||
await Effect.runPromise(
|
||||
publisher.toolExecution(localCall.id, localCall.name, {
|
||||
publisher.toolExecution(call.id, call.name, {
|
||||
output: { type: "media", mime: "image/png" },
|
||||
content: [
|
||||
{ type: "text", text: "Image read successfully" },
|
||||
@@ -83,11 +84,6 @@ test("local tool success serializes media base64 once through canonical content"
|
||||
expect(success?.data).not.toHaveProperty("result")
|
||||
expect(success?.data).not.toHaveProperty("output")
|
||||
|
||||
const called = published.find((event) => event.type === "session.tool.called.1")?.data
|
||||
expect(called).toMatchObject({ state: { itemId: "fc_call-image" } })
|
||||
expect(success?.data).not.toHaveProperty("resultState")
|
||||
expect(JSON.stringify(success?.data)).not.toContain('"itemId":"fc_call-image"')
|
||||
|
||||
expect(success?.data).toMatchObject({
|
||||
content: [
|
||||
{ type: "text", text: "Image read successfully" },
|
||||
@@ -230,7 +226,9 @@ test("provider-executed tool metadata is flattened using the route key", async (
|
||||
test("binary failure emits no success event", async () => {
|
||||
const { published, publisher } = capture()
|
||||
await Effect.runPromise(publisher.publish(call))
|
||||
await Effect.runPromise(publisher.failTool(call.id, { type: "tool.execution", message: "Cannot read binary file" }))
|
||||
await Effect.runPromise(
|
||||
publisher.failTool(call.id, { type: "tool.execution", message: "Cannot read binary file" }),
|
||||
)
|
||||
expect(published.some((event) => event.type === "session.tool.success.2")).toBe(false)
|
||||
expect(published.some((event) => event.type === "session.tool.failed.2")).toBe(true)
|
||||
})
|
||||
|
||||
@@ -558,15 +558,6 @@ const messageTexts = (request: LLMRequest, role: "user" | "system") =>
|
||||
const userTexts = (request: LLMRequest) => messageTexts(request, "user")
|
||||
const systemTexts = (request: LLMRequest) => messageTexts(request, "system")
|
||||
const messageRoles = (request: LLMRequest | undefined) => request?.messages.map((message) => message.role)
|
||||
const withoutItemIDs = (messages: LLMRequest["messages"]) =>
|
||||
messages.map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content.map((part) => {
|
||||
if (!("itemId" in part)) return part
|
||||
const { itemId: _itemId, ...content } = part
|
||||
return content
|
||||
}),
|
||||
}))
|
||||
|
||||
const recordedEventTypes = (id: Session.ID) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -865,8 +856,8 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* Fiber.join(renamed)
|
||||
|
||||
expect(requests).toHaveLength(5)
|
||||
expect(withoutItemIDs(requests[2]!.messages)).toContainEqual(withoutItemIDs([Message.user("First prompt")])[0])
|
||||
expect(withoutItemIDs(requests[4]!.messages)).toContainEqual(withoutItemIDs([Message.user("First prompt")])[0])
|
||||
expect(requests[2]?.messages).toContainEqual(Message.user("First prompt"))
|
||||
expect(requests[4]?.messages).toContainEqual(Message.user("First prompt"))
|
||||
expect((yield* session.get(sessionID)).title).toBe("Generated title")
|
||||
}),
|
||||
)
|
||||
@@ -891,7 +882,7 @@ describe("SessionRunnerLLM", () => {
|
||||
// A hook-removed call fails independently and continues while step allowance remains.
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests[0]?.system.map((part) => part.text)).toEqual(["Hooked system"])
|
||||
expect(withoutItemIDs(requests[0]!.messages)).toEqual(withoutItemIDs([Message.user("Hooked message")]))
|
||||
expect(requests[0]?.messages).toEqual([Message.user("Hooked message")])
|
||||
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("echo")
|
||||
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("unregistered")
|
||||
expect(executions).toEqual([])
|
||||
@@ -1316,14 +1307,12 @@ describe("SessionRunnerLLM", () => {
|
||||
systemBaseline = "Changed context"
|
||||
yield* runPrompt(session, "Second")
|
||||
|
||||
const firstSnapshot = PromptCacheDiagnostics.snapshot(requests[0]!)
|
||||
const secondSnapshot = PromptCacheDiagnostics.snapshot(requests[1]!)
|
||||
expect(PromptCacheDiagnostics.compare(firstSnapshot, secondSnapshot)).toEqual({
|
||||
status: "append-only",
|
||||
previousMessages: 1,
|
||||
currentMessages: 3,
|
||||
})
|
||||
expect(secondSnapshot.messages[0]).toEqual(firstSnapshot.messages[0])
|
||||
expect(
|
||||
PromptCacheDiagnostics.compare(
|
||||
PromptCacheDiagnostics.snapshot(requests[0]),
|
||||
PromptCacheDiagnostics.snapshot(requests[1]),
|
||||
),
|
||||
).toEqual({ status: "append-only", previousMessages: 1, currentMessages: 3 })
|
||||
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
|
||||
[defaultSystem, "Initial context"],
|
||||
[defaultSystem, "Initial context"],
|
||||
@@ -2543,24 +2532,9 @@ describe("SessionRunnerLLM", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Encrypted thought",
|
||||
itemId: "rs_1",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
])
|
||||
|
||||
yield* admit(session, "Continue again")
|
||||
yield* TestLLM.push([])
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests[2]?.messages[1]?.content.map((part) => ("itemId" in part ? part.itemId : undefined))).toEqual(
|
||||
requests[1]?.messages[1]?.content.map((part) => ("itemId" in part ? part.itemId : undefined)),
|
||||
)
|
||||
expect(
|
||||
PromptCacheDiagnostics.compare(
|
||||
PromptCacheDiagnostics.snapshot(requests[1]!),
|
||||
PromptCacheDiagnostics.snapshot(requests[2]!),
|
||||
),
|
||||
).toEqual({ status: "append-only", previousMessages: 3, currentMessages: 4 })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -2639,7 +2613,6 @@ describe("SessionRunnerLLM", () => {
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "hosted-search",
|
||||
itemId: "hosted-search",
|
||||
name: "web_search",
|
||||
input: { query: "Effect" },
|
||||
providerExecuted: true,
|
||||
@@ -3281,7 +3254,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* stream.started
|
||||
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests.map((request) => request.providerOptions?.openai?.promptCacheKey)).toEqual([
|
||||
expect(requests.map((request) => request.promptCacheKey)).toEqual([
|
||||
sessionID,
|
||||
otherSessionID,
|
||||
])
|
||||
@@ -3312,7 +3285,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* session.resume(longSessionID)
|
||||
yield* session.resume(otherLongSessionID)
|
||||
|
||||
const keys = requests.map((request) => request.providerOptions?.openai?.promptCacheKey)
|
||||
const keys = requests.map((request) => request.promptCacheKey)
|
||||
expect(keys).toEqual([longSessionID.slice(4), otherLongSessionID.slice(4)])
|
||||
expect(keys.every((key) => typeof key === "string" && key.length === 64)).toBe(true)
|
||||
expect(keys[0]).not.toBe(keys[1])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user