@opencode-ai/ai 0.0.0-next-16134 → 0.0.0-next-16144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,756 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { HttpTransport } from "../route/transport";
3
+ import { Protocol } from "../route/protocol";
4
+ import { LLMError, LLMEvent, Usage, } from "../schema";
5
+ import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared";
6
+ import { classifyProviderFailure } from "../provider-error";
7
+ import { OpenResponsesOptions } from "./utils/open-responses-options";
8
+ import { Lifecycle } from "./utils/lifecycle";
9
+ import { ToolSchemaProjection } from "./utils/tool-schema";
10
+ import { ToolStream } from "./utils/tool-stream";
11
+ const ADAPTER = "open-responses";
12
+ const NAME = "Open Responses";
13
+ const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]);
14
+ export const PATH = "/responses";
15
+ // =============================================================================
16
+ // Request Body Schema
17
+ // =============================================================================
18
+ const OpenResponsesInputText = Schema.Struct({
19
+ type: Schema.tag("input_text"),
20
+ text: Schema.String,
21
+ });
22
+ const OpenResponsesInputImage = Schema.Struct({
23
+ type: Schema.tag("input_image"),
24
+ image_url: Schema.String,
25
+ });
26
+ const OpenResponsesInputFile = Schema.Struct({
27
+ type: Schema.tag("input_file"),
28
+ filename: Schema.String,
29
+ file_data: Schema.String,
30
+ mime_type: Schema.optional(Schema.String),
31
+ });
32
+ const MediaInput = Schema.Union([OpenResponsesInputImage, OpenResponsesInputFile]);
33
+ const OpenResponsesInputContent = Schema.Union([OpenResponsesInputText, MediaInput]);
34
+ const OpenResponsesOutputText = Schema.Struct({
35
+ type: Schema.tag("output_text"),
36
+ text: Schema.String,
37
+ });
38
+ const OpenResponsesReasoningSummaryText = Schema.Struct({
39
+ type: Schema.tag("summary_text"),
40
+ text: Schema.String,
41
+ });
42
+ const OpenResponsesReasoningItem = Schema.Struct({
43
+ type: Schema.tag("reasoning"),
44
+ id: Schema.optionalKey(Schema.String),
45
+ summary: Schema.Array(OpenResponsesReasoningSummaryText),
46
+ encrypted_content: optionalNull(Schema.String),
47
+ });
48
+ const OpenResponsesItemReference = Schema.Struct({
49
+ type: Schema.tag("item_reference"),
50
+ id: Schema.String,
51
+ });
52
+ // `function_call_output.output` accepts either a plain string or an ordered
53
+ // array of content items so tools can return images and files in addition to text.
54
+ // https://www.openresponses.org/reference
55
+ const OpenResponsesFunctionCallOutputContent = Schema.Union([
56
+ OpenResponsesInputText,
57
+ OpenResponsesInputImage,
58
+ OpenResponsesInputFile,
59
+ ]);
60
+ const OpenResponsesFunctionCallOutput = Schema.Union([
61
+ Schema.String,
62
+ Schema.Array(OpenResponsesFunctionCallOutputContent),
63
+ ]);
64
+ const OpenResponsesInputItem = Schema.Union([
65
+ Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
66
+ Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }),
67
+ Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenResponsesOutputText) }),
68
+ OpenResponsesReasoningItem,
69
+ OpenResponsesItemReference,
70
+ Schema.Struct({
71
+ type: Schema.tag("function_call"),
72
+ call_id: Schema.String,
73
+ name: Schema.String,
74
+ arguments: Schema.String,
75
+ }),
76
+ Schema.Struct({
77
+ type: Schema.tag("function_call_output"),
78
+ call_id: Schema.String,
79
+ output: OpenResponsesFunctionCallOutput,
80
+ }),
81
+ ]);
82
+ export const Tool = Schema.Struct({
83
+ type: Schema.tag("function"),
84
+ name: Schema.String,
85
+ description: Schema.String,
86
+ parameters: JsonObject,
87
+ strict: Schema.optional(Schema.Boolean),
88
+ });
89
+ export const ToolChoice = Schema.Union([
90
+ Schema.Literals(["auto", "none", "required"]),
91
+ Schema.Struct({ type: Schema.tag("function"), name: Schema.String }),
92
+ ]);
93
+ // Fields shared between the HTTP body and the WebSocket `response.create`
94
+ // message. The HTTP body adds `stream: true`; the WebSocket message adds
95
+ // `type: "response.create"`. Defining the shared shape once keeps the two
96
+ // transports in sync without a destructure-and-strip dance.
97
+ export const coreFields = {
98
+ model: Schema.String,
99
+ input: Schema.Array(OpenResponsesInputItem),
100
+ instructions: Schema.optional(Schema.String),
101
+ tools: optionalArray(Tool),
102
+ tool_choice: Schema.optional(ToolChoice),
103
+ store: Schema.optional(Schema.Boolean),
104
+ service_tier: Schema.optional(OpenResponsesOptions.ServiceTierSchema),
105
+ prompt_cache_key: Schema.optional(Schema.String),
106
+ include: optionalArray(OpenResponsesOptions.ResponseIncludableSchema),
107
+ reasoning: Schema.optional(Schema.Struct({
108
+ effort: Schema.optional(OpenResponsesOptions.ReasoningEffort),
109
+ summary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])),
110
+ })),
111
+ text: Schema.optional(Schema.Struct({
112
+ verbosity: Schema.optional(OpenResponsesOptions.TextVerbositySchema),
113
+ })),
114
+ max_output_tokens: Schema.optional(Schema.Number),
115
+ temperature: Schema.optional(Schema.Number),
116
+ top_p: Schema.optional(Schema.Number),
117
+ };
118
+ const OpenResponsesBody = Schema.Struct({
119
+ ...coreFields,
120
+ stream: Schema.Literal(true),
121
+ });
122
+ const OpenResponsesUsage = Schema.Struct({
123
+ input_tokens: Schema.optional(Schema.Number),
124
+ input_tokens_details: optionalNull(Schema.Struct({ cached_tokens: Schema.optional(Schema.Number) })),
125
+ output_tokens: Schema.optional(Schema.Number),
126
+ output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })),
127
+ total_tokens: Schema.optional(Schema.Number),
128
+ });
129
+ export const StreamItem = Schema.StructWithRest(Schema.Struct({
130
+ type: Schema.String,
131
+ id: Schema.optional(Schema.String),
132
+ call_id: Schema.optional(Schema.String),
133
+ name: Schema.optional(Schema.String),
134
+ arguments: Schema.optional(Schema.String),
135
+ encrypted_content: optionalNull(Schema.String),
136
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
137
+ // The Responses schema puts streaming error details at the top level and
138
+ // response failures under `response.error`. WebSocket failures use an
139
+ // event-level `error` envelope, so accept all three shapes here.
140
+ // https://www.openresponses.org/specification
141
+ const OpenResponsesErrorPayload = Schema.Struct({
142
+ code: optionalNull(Schema.String),
143
+ message: optionalNull(Schema.String),
144
+ param: optionalNull(Schema.String),
145
+ });
146
+ export const Event = Schema.StructWithRest(Schema.Struct({
147
+ type: Schema.String,
148
+ delta: Schema.optional(Schema.String),
149
+ item_id: Schema.optional(Schema.String),
150
+ summary_index: Schema.optional(Schema.Number),
151
+ item: Schema.optional(StreamItem),
152
+ response: Schema.optional(Schema.StructWithRest(Schema.Struct({
153
+ id: Schema.optional(Schema.String),
154
+ service_tier: optionalNull(Schema.String),
155
+ incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })),
156
+ usage: optionalNull(OpenResponsesUsage),
157
+ error: optionalNull(OpenResponsesErrorPayload),
158
+ }), [Schema.Record(Schema.String, Schema.Unknown)])),
159
+ code: optionalNull(Schema.String),
160
+ message: Schema.optional(Schema.String),
161
+ param: optionalNull(Schema.String),
162
+ error: optionalNull(OpenResponsesErrorPayload),
163
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
164
+ const BASE = { id: ADAPTER, name: NAME };
165
+ // =============================================================================
166
+ // Request Lowering
167
+ // =============================================================================
168
+ export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* (protocolName, tool, inputSchema) {
169
+ if (tool.native !== undefined)
170
+ return yield* ProviderShared.invalidRequest(`${protocolName} does not support provider-native tool ${tool.name}`);
171
+ return {
172
+ type: "function",
173
+ name: tool.name,
174
+ description: tool.description,
175
+ parameters: ToolSchemaProjection.responses(inputSchema),
176
+ // TODO: Read this from Responses tool options so direct LLM callers can opt into strict schemas.
177
+ strict: false,
178
+ };
179
+ });
180
+ export const lowerToolChoice = (protocolName, toolChoice) => ProviderShared.matchToolChoice(protocolName, toolChoice, {
181
+ auto: () => "auto",
182
+ none: () => "none",
183
+ required: () => "required",
184
+ tool: (toolName) => ({ type: "function", name: toolName }),
185
+ });
186
+ const lowerToolCall = (part) => ({
187
+ type: "function_call",
188
+ call_id: part.id,
189
+ name: part.name,
190
+ arguments: ProviderShared.encodeJson(part.input),
191
+ });
192
+ const lowerReasoning = (part, providerMetadataKey) => {
193
+ const metadata = part.providerMetadata?.[providerMetadataKey];
194
+ if (!ProviderShared.isRecord(metadata) || typeof metadata.itemId !== "string" || metadata.itemId.length === 0)
195
+ return undefined;
196
+ const encryptedContent = typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
197
+ ? metadata.reasoningEncryptedContent
198
+ : undefined;
199
+ return {
200
+ type: "reasoning",
201
+ id: metadata.itemId,
202
+ summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
203
+ encrypted_content: encryptedContent,
204
+ };
205
+ };
206
+ const hostedToolItemID = (part, providerMetadataKey) => {
207
+ const metadata = part.providerMetadata?.[providerMetadataKey];
208
+ return ProviderShared.isRecord(metadata) && typeof metadata.itemId === "string" && metadata.itemId.length > 0
209
+ ? metadata.itemId
210
+ : undefined;
211
+ };
212
+ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (part, request, extension) {
213
+ const media = yield* ProviderShared.validateMedia(extension.name, part, MEDIA_MIMES);
214
+ const extended = extension.lowerMedia?.({ part, media, request });
215
+ if (extended)
216
+ return extended;
217
+ if (media.mime === "application/pdf") {
218
+ return {
219
+ type: "input_file",
220
+ filename: part.filename ?? "document.pdf",
221
+ file_data: media.dataUrl,
222
+ };
223
+ }
224
+ return { type: "input_image", image_url: media.dataUrl };
225
+ });
226
+ const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* (part, request, extension) {
227
+ if (part.type === "text")
228
+ return { type: "input_text", text: part.text };
229
+ if (part.type === "media")
230
+ return yield* lowerMedia(part, request, extension);
231
+ return yield* ProviderShared.unsupportedContent(extension.name, "user", ["text", "media"]);
232
+ });
233
+ // Tool results may carry structured text, images, and files. Keep media as provider-native
234
+ // content instead of JSON-stringifying base64 into a prompt string.
235
+ const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* (item, request, extension) {
236
+ if (item.type === "text")
237
+ return { type: "input_text", text: item.text };
238
+ return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, request, extension);
239
+ });
240
+ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(function* (part, request, extension) {
241
+ // Text/json/error results are encoded as a plain string for backward
242
+ // compatibility with existing cassettes and provider expectations.
243
+ if (part.result.type !== "content")
244
+ return ProviderShared.toolResultText(part);
245
+ // Preserve the narrowed array element type when compiled through a consumer package.
246
+ const content = part.result.value;
247
+ return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension));
248
+ });
249
+ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request, extension) {
250
+ const system = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }];
251
+ const input = [...system];
252
+ const store = OpenResponsesOptions.resolve(request).store;
253
+ const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses";
254
+ for (const message of request.messages) {
255
+ if (message.role === "system") {
256
+ const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message);
257
+ const previous = input.at(-1);
258
+ if (previous && "role" in previous && previous.role === "user")
259
+ input[input.length - 1] = {
260
+ role: "user",
261
+ content: [...previous.content, { type: "input_text", text: part.text }],
262
+ };
263
+ else
264
+ input.push({ role: "user", content: [{ type: "input_text", text: part.text }] });
265
+ continue;
266
+ }
267
+ if (message.role === "user") {
268
+ input.push({
269
+ role: "user",
270
+ content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request, extension)),
271
+ });
272
+ continue;
273
+ }
274
+ if (message.role === "assistant") {
275
+ const content = [];
276
+ const reasoningItems = {};
277
+ const reasoningReferences = new Set();
278
+ const hostedToolReferences = new Set();
279
+ const flushText = () => {
280
+ if (content.length === 0)
281
+ return;
282
+ input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) });
283
+ content.splice(0, content.length);
284
+ };
285
+ for (const part of message.content) {
286
+ if (part.type === "text") {
287
+ content.push(part);
288
+ continue;
289
+ }
290
+ if (part.type === "reasoning") {
291
+ flushText();
292
+ const reasoning = lowerReasoning(part, providerMetadataKey);
293
+ if (!reasoning)
294
+ continue;
295
+ if (store !== false) {
296
+ if (!reasoningReferences.has(reasoning.id))
297
+ input.push({ type: "item_reference", id: reasoning.id });
298
+ reasoningReferences.add(reasoning.id);
299
+ continue;
300
+ }
301
+ const existing = reasoningItems[reasoning.id];
302
+ if (existing) {
303
+ existing.summary.push(...reasoning.summary);
304
+ if (typeof reasoning.encrypted_content === "string")
305
+ existing.encrypted_content = reasoning.encrypted_content;
306
+ continue;
307
+ }
308
+ const replay = {
309
+ type: reasoning.type,
310
+ summary: reasoning.summary,
311
+ encrypted_content: reasoning.encrypted_content,
312
+ };
313
+ reasoningItems[reasoning.id] = replay;
314
+ input.push(replay);
315
+ continue;
316
+ }
317
+ if (part.type === "tool-call") {
318
+ flushText();
319
+ if (part.providerExecuted === true)
320
+ continue;
321
+ input.push(lowerToolCall(part));
322
+ continue;
323
+ }
324
+ if (part.type === "tool-result" && part.providerExecuted === true) {
325
+ flushText();
326
+ const itemID = hostedToolItemID(part, providerMetadataKey);
327
+ if (store !== false && itemID && !hostedToolReferences.has(itemID))
328
+ input.push({ type: "item_reference", id: itemID });
329
+ if (store === false && part.result.type === "content") {
330
+ const content = part.result.value;
331
+ input.push({
332
+ role: "user",
333
+ content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)),
334
+ });
335
+ }
336
+ if (itemID)
337
+ hostedToolReferences.add(itemID);
338
+ continue;
339
+ }
340
+ return yield* ProviderShared.unsupportedContent(extension.name, "assistant", [
341
+ "text",
342
+ "reasoning",
343
+ "tool-call",
344
+ "tool-result",
345
+ ]);
346
+ }
347
+ flushText();
348
+ continue;
349
+ }
350
+ for (const part of message.content) {
351
+ if (!ProviderShared.supportsContent(part, ["tool-result"]))
352
+ return yield* ProviderShared.unsupportedContent(extension.name, "tool", ["tool-result"]);
353
+ input.push({
354
+ type: "function_call_output",
355
+ call_id: part.id,
356
+ output: yield* lowerToolResultOutput(part, request, extension),
357
+ });
358
+ }
359
+ }
360
+ // With store:false, Responses APIs only accept previous reasoning items when the
361
+ // complete item has encrypted state. Summary blocks for one item may carry
362
+ // that state only on the last block, so filter after they have been joined.
363
+ return store === false
364
+ ? input.filter((item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string")
365
+ : input;
366
+ });
367
+ const lowerOptions = (request) => {
368
+ const options = OpenResponsesOptions.resolve(request);
369
+ return {
370
+ ...(options.instructions ? { instructions: options.instructions } : {}),
371
+ ...(options.store !== undefined ? { store: options.store } : {}),
372
+ ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
373
+ ...(options.include ? { include: options.include } : {}),
374
+ ...(options.reasoningEffort || options.reasoningSummary
375
+ ? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
376
+ : {}),
377
+ ...(options.textVerbosity ? { text: { verbosity: options.textVerbosity } } : {}),
378
+ ...(options.serviceTier ? { service_tier: options.serviceTier } : {}),
379
+ };
380
+ };
381
+ export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (request, extension = BASE) {
382
+ const generation = request.generation;
383
+ const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
384
+ return {
385
+ model: request.model.id,
386
+ input: yield* lowerMessages(request, extension),
387
+ tools: request.tools.length === 0
388
+ ? undefined
389
+ : yield* Effect.forEach(request.tools, (tool) => lowerTool(extension.name, tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
390
+ tool_choice: request.toolChoice ? yield* lowerToolChoice(extension.name, request.toolChoice) : undefined,
391
+ stream: true,
392
+ max_output_tokens: generation?.maxTokens,
393
+ temperature: generation?.temperature,
394
+ top_p: generation?.topP,
395
+ ...lowerOptions(request),
396
+ };
397
+ });
398
+ // =============================================================================
399
+ // Stream Parsing
400
+ // =============================================================================
401
+ // Responses APIs report `input_tokens` (inclusive total) with a
402
+ // `cached_tokens` subset, and `output_tokens` (inclusive total) with a
403
+ // `reasoning_tokens` subset. Pass the totals through and derive the
404
+ // non-cached breakdown.
405
+ const mapUsage = (usage, providerMetadataKey) => {
406
+ if (!usage)
407
+ return undefined;
408
+ const cached = usage.input_tokens_details?.cached_tokens;
409
+ const reasoning = usage.output_tokens_details?.reasoning_tokens;
410
+ const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached);
411
+ return new Usage({
412
+ inputTokens: usage.input_tokens,
413
+ outputTokens: usage.output_tokens,
414
+ nonCachedInputTokens: nonCached,
415
+ cacheReadInputTokens: cached,
416
+ reasoningTokens: reasoning,
417
+ totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
418
+ providerMetadata: { [providerMetadataKey]: usage },
419
+ });
420
+ };
421
+ const mapFinishReason = (event, hasFunctionCall) => {
422
+ const reason = event.response?.incomplete_details?.reason;
423
+ if (reason === undefined || reason === null) {
424
+ if (hasFunctionCall)
425
+ return "tool-calls";
426
+ if (event.type === "response.incomplete")
427
+ return "unknown";
428
+ return "stop";
429
+ }
430
+ if (reason === "max_output_tokens")
431
+ return "length";
432
+ if (reason === "content_filter")
433
+ return "content-filter";
434
+ return hasFunctionCall ? "tool-calls" : "unknown";
435
+ };
436
+ export const providerMetadata = (state, metadata) => ({
437
+ [state.providerMetadataKey]: metadata,
438
+ });
439
+ const isReasoningItem = (item) => item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0;
440
+ const NO_EVENTS = [];
441
+ // `response.completed` / `response.incomplete` are clean finishes that emit a
442
+ // `finish` event; `response.failed` is a hard failure. All three end the stream,
443
+ // so keep this set aligned with `step` and the protocol's terminal predicate.
444
+ const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]);
445
+ export const terminal = (event) => TERMINAL_TYPES.has(event.type);
446
+ const onOutputTextDelta = (state, event) => {
447
+ if (!event.delta)
448
+ return [state, NO_EVENTS];
449
+ const events = [];
450
+ return [
451
+ { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) },
452
+ events,
453
+ ];
454
+ };
455
+ const onOutputTextDone = (state, event) => {
456
+ const events = [];
457
+ return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, event.item_id ?? "text-0") }, events];
458
+ };
459
+ export const onReasoningDelta = (state, event) => {
460
+ if (!event.delta)
461
+ return [state, NO_EVENTS];
462
+ const events = [];
463
+ const itemID = event.item_id ?? "reasoning-0";
464
+ const id = event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID;
465
+ return [
466
+ {
467
+ ...state,
468
+ lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
469
+ },
470
+ events,
471
+ ];
472
+ };
473
+ export const onReasoningDone = (state, _event) => [state, NO_EVENTS];
474
+ const reasoningMetadata = (state, item) => providerMetadata(state, { itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null });
475
+ // Responses APIs stream reasoning items in a stable order:
476
+ // `output_item.added` (reasoning) →
477
+ // `reasoning_summary_part.added` (index=0) →
478
+ // `reasoning_summary_text.delta` →
479
+ // `reasoning_summary_part.done` (index=0) →
480
+ // (repeat for index>0) →
481
+ // `output_item.done` (reasoning).
482
+ // The handlers below rely on this ordering: `onOutputItemAdded` seeds the
483
+ // per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0`
484
+ // short-circuits when the entry already exists, and higher-index handlers
485
+ // fold against the same entry. Behaviour for out-of-order events is
486
+ // best-effort, not guaranteed.
487
+ const onOutputItemAdded = (state, event) => {
488
+ const item = event.item;
489
+ if (item && isReasoningItem(item)) {
490
+ const events = [];
491
+ return [
492
+ {
493
+ ...state,
494
+ lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(state, item)),
495
+ reasoningItems: {
496
+ ...state.reasoningItems,
497
+ [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
498
+ },
499
+ },
500
+ events,
501
+ ];
502
+ }
503
+ if (item?.type !== "function_call" || !item.id)
504
+ return [state, NO_EVENTS];
505
+ const metadata = providerMetadata(state, { itemId: item.id });
506
+ const events = [];
507
+ const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
508
+ return [
509
+ {
510
+ ...state,
511
+ lifecycle,
512
+ tools: ToolStream.start(state.tools, item.id, {
513
+ id: item.call_id ?? item.id,
514
+ name: item.name ?? "",
515
+ input: item.arguments ?? "",
516
+ providerMetadata: metadata,
517
+ }),
518
+ },
519
+ [
520
+ ...events,
521
+ LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata: metadata }),
522
+ ],
523
+ ];
524
+ };
525
+ const onReasoningSummaryPartAdded = (state, event) => {
526
+ if (!event.item_id || event.summary_index === undefined)
527
+ return [state, NO_EVENTS];
528
+ const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} };
529
+ if (event.summary_index === 0) {
530
+ if (state.reasoningItems[event.item_id])
531
+ return [state, NO_EVENTS];
532
+ const events = [];
533
+ return [
534
+ {
535
+ ...state,
536
+ lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${event.item_id}:0`, providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: null })),
537
+ reasoningItems: {
538
+ ...state.reasoningItems,
539
+ [event.item_id]: { ...item, summaryParts: { 0: "active" } },
540
+ },
541
+ },
542
+ events,
543
+ ];
544
+ }
545
+ const events = [];
546
+ const closed = Object.entries(item.summaryParts)
547
+ .filter((entry) => entry[1] === "can-conclude")
548
+ .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${event.item_id}:${entry[0]}`, providerMetadata(state, { itemId: event.item_id })), state.lifecycle);
549
+ return [
550
+ {
551
+ ...state,
552
+ lifecycle: Lifecycle.reasoningStart(closed, events, `${event.item_id}:${event.summary_index}`, providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null })),
553
+ reasoningItems: {
554
+ ...state.reasoningItems,
555
+ [event.item_id]: {
556
+ ...item,
557
+ summaryParts: {
558
+ ...Object.fromEntries(Object.entries(item.summaryParts).map((entry) => entry[1] === "can-conclude" ? [entry[0], "concluded"] : entry)),
559
+ [event.summary_index]: "active",
560
+ },
561
+ },
562
+ },
563
+ },
564
+ events,
565
+ ];
566
+ };
567
+ const onReasoningSummaryPartDone = (state, event) => {
568
+ if (!event.item_id || event.summary_index === undefined)
569
+ return [state, NO_EVENTS];
570
+ const item = state.reasoningItems[event.item_id];
571
+ if (!item)
572
+ return [state, NO_EVENTS];
573
+ const events = [];
574
+ return [
575
+ {
576
+ ...state,
577
+ lifecycle: state.store !== false
578
+ ? Lifecycle.reasoningEnd(state.lifecycle, events, `${event.item_id}:${event.summary_index}`, providerMetadata(state, { itemId: event.item_id }))
579
+ : state.lifecycle,
580
+ reasoningItems: {
581
+ ...state.reasoningItems,
582
+ [event.item_id]: {
583
+ ...item,
584
+ summaryParts: {
585
+ ...item.summaryParts,
586
+ [event.summary_index]: state.store !== false ? "concluded" : "can-conclude",
587
+ },
588
+ },
589
+ },
590
+ },
591
+ events,
592
+ ];
593
+ };
594
+ const onFunctionCallArgumentsDelta = Effect.fn("OpenResponses.onFunctionCallArgumentsDelta")(function* (state, event) {
595
+ if (!event.item_id || !event.delta)
596
+ return [state, NO_EVENTS];
597
+ const result = ToolStream.appendExisting(state.id, state.tools, event.item_id, event.delta, `${state.name} tool argument delta is missing its tool call`);
598
+ if (ToolStream.isError(result))
599
+ return yield* result;
600
+ const events = [];
601
+ const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
602
+ events.push(...result.events);
603
+ return [{ ...state, lifecycle, tools: result.tools }, events];
604
+ });
605
+ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (state, event) {
606
+ const item = event.item;
607
+ if (!item)
608
+ return [state, NO_EVENTS];
609
+ if (item.type === "message" && item.id)
610
+ return onOutputTextDone(state, { ...event, item_id: item.id });
611
+ if (item.type === "function_call") {
612
+ if (!item.id || !item.call_id || !item.name)
613
+ return [state, NO_EVENTS];
614
+ const tools = state.tools[item.id]
615
+ ? state.tools
616
+ : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name });
617
+ const result = item.arguments === undefined
618
+ ? yield* ToolStream.finish(state.id, tools, item.id)
619
+ : yield* ToolStream.finishWithInput(state.id, tools, item.id, item.arguments);
620
+ const events = [];
621
+ const resultEvents = result.events ?? [];
622
+ const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
623
+ events.push(...resultEvents);
624
+ return [
625
+ {
626
+ ...state,
627
+ lifecycle,
628
+ hasFunctionCall: resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) ||
629
+ state.hasFunctionCall,
630
+ tools: result.tools,
631
+ },
632
+ events,
633
+ ];
634
+ }
635
+ if (isReasoningItem(item)) {
636
+ const events = [];
637
+ const metadata = reasoningMetadata(state, item);
638
+ const reasoningItem = state.reasoningItems[item.id];
639
+ if (reasoningItem) {
640
+ const lifecycle = Object.entries(reasoningItem.summaryParts)
641
+ .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
642
+ .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, metadata), state.lifecycle);
643
+ const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems;
644
+ return [{ ...state, lifecycle, reasoningItems }, events];
645
+ }
646
+ if (!state.lifecycle.reasoning.has(item.id)) {
647
+ const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
648
+ events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata: metadata }));
649
+ events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata: metadata }));
650
+ return [{ ...state, lifecycle }, events];
651
+ }
652
+ return [
653
+ { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, metadata) },
654
+ events,
655
+ ];
656
+ }
657
+ return [state, NO_EVENTS];
658
+ });
659
+ const onResponseFinish = (state, event) => {
660
+ const events = [];
661
+ const lifecycle = Lifecycle.finish(state.lifecycle, events, {
662
+ reason: {
663
+ normalized: mapFinishReason(event, state.hasFunctionCall),
664
+ raw: event.response?.incomplete_details?.reason,
665
+ },
666
+ usage: mapUsage(event.response?.usage, state.providerMetadataKey),
667
+ providerMetadata: event.response?.id || event.response?.service_tier
668
+ ? providerMetadata(state, {
669
+ responseId: event.response.id,
670
+ serviceTier: event.response.service_tier,
671
+ })
672
+ : undefined,
673
+ });
674
+ return [{ ...state, lifecycle }, events];
675
+ };
676
+ // Build a single human-readable message from whatever the provider supplied.
677
+ // When both code and message are present, prefix the code so consumers see
678
+ // the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just
679
+ // the bare message — production rate limits and context-length failures used
680
+ // to be indistinguishable from generic stream drops.
681
+ const providerErrorMessage = (event, fallback) => {
682
+ const nested = event.error ?? event.response?.error ?? undefined;
683
+ const message = event.message || nested?.message || undefined;
684
+ const code = event.code || nested?.code || undefined;
685
+ if (message && code)
686
+ return `${code}: ${message}`;
687
+ return message || code || fallback;
688
+ };
689
+ const providerError = (state, event, fallback) => {
690
+ const code = event.code || event.error?.code || event.response?.error?.code || undefined;
691
+ const message = providerErrorMessage(event, fallback);
692
+ return new LLMError({
693
+ module: state.id,
694
+ method: "stream",
695
+ reason: classifyProviderFailure({ message, code }),
696
+ });
697
+ };
698
+ export const step = (state, event) => {
699
+ if (event.type === "response.output_text.delta")
700
+ return Effect.succeed(onOutputTextDelta(state, event));
701
+ if (event.type === "response.output_text.done")
702
+ return Effect.succeed(onOutputTextDone(state, event));
703
+ if (event.type === "response.reasoning.delta" || event.type === "response.reasoning_summary_text.delta")
704
+ return Effect.succeed(onReasoningDelta(state, event));
705
+ if (event.type === "response.reasoning.done" || event.type === "response.reasoning_summary_text.done")
706
+ return Effect.succeed(onReasoningDone(state, event));
707
+ if (event.type === "response.reasoning_summary_part.added")
708
+ return Effect.succeed(onReasoningSummaryPartAdded(state, event));
709
+ if (event.type === "response.reasoning_summary_part.done")
710
+ return Effect.succeed(onReasoningSummaryPartDone(state, event));
711
+ if (event.type === "response.output_item.added")
712
+ return Effect.succeed(onOutputItemAdded(state, event));
713
+ if (event.type === "response.function_call_arguments.delta")
714
+ return onFunctionCallArgumentsDelta(state, event);
715
+ if (event.type === "response.output_item.done")
716
+ return onOutputItemDone(state, event);
717
+ if (event.type === "response.completed" || event.type === "response.incomplete")
718
+ return Effect.succeed(onResponseFinish(state, event));
719
+ if (event.type === "response.failed")
720
+ return providerError(state, event, `${state.name} response failed`);
721
+ if (event.type === "error")
722
+ return providerError(state, event, `${state.name} stream error`);
723
+ return Effect.succeed([state, NO_EVENTS]);
724
+ };
725
+ // =============================================================================
726
+ // Protocol
727
+ // =============================================================================
728
+ /**
729
+ * The provider-neutral Open Responses protocol. Provider-specific Responses
730
+ * implementations compose this baseline with their own tools and event variants.
731
+ */
732
+ export const initial = (request, extension = BASE) => ({
733
+ id: extension.id,
734
+ name: extension.name,
735
+ providerMetadataKey: request.model.route.providerMetadataKey ?? "openresponses",
736
+ hasFunctionCall: false,
737
+ tools: ToolStream.empty(),
738
+ lifecycle: Lifecycle.initial(),
739
+ reasoningItems: {},
740
+ store: OpenResponsesOptions.resolve(request).store,
741
+ });
742
+ export const protocol = Protocol.make({
743
+ id: ADAPTER,
744
+ body: {
745
+ schema: OpenResponsesBody,
746
+ from: fromRequest,
747
+ },
748
+ stream: {
749
+ event: Protocol.jsonEvent(Event),
750
+ initial,
751
+ step,
752
+ terminal,
753
+ },
754
+ });
755
+ export const httpTransport = HttpTransport.sseJson.with();
756
+ export * as OpenResponses from "./open-responses";