@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.
@@ -2,97 +2,18 @@ import { Effect, Encoding, Schema } from "effect";
2
2
  import { Route } from "../route/client";
3
3
  import { Auth } from "../route/auth";
4
4
  import { Endpoint } from "../route/endpoint";
5
- import { HttpTransport, WebSocketTransport } from "../route/transport";
6
5
  import { Protocol } from "../route/protocol";
7
- import { LLMError, LLMEvent, Usage, } from "../schema";
8
- import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared";
9
- import { classifyProviderFailure } from "../provider-error";
10
- import { OpenAIOptions } from "./utils/openai-options";
6
+ import { HttpTransport, WebSocketTransport } from "../route/transport";
7
+ import { LLMEvent, LLMRequest } from "../schema";
8
+ import { OpenResponses } from "./open-responses";
9
+ import { optionalArray, ProviderShared } from "./shared";
11
10
  import { Lifecycle } from "./utils/lifecycle";
12
- import { ToolSchemaProjection } from "./utils/tool-schema";
13
- import { ToolStream } from "./utils/tool-stream";
14
11
  import { OpenAIImage } from "./utils/openai-image";
12
+ import { ToolSchemaProjection } from "./utils/tool-schema";
15
13
  const ADAPTER = "openai-responses";
16
- const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]);
14
+ const NAME = "OpenAI Responses";
17
15
  export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
18
- export const PATH = "/responses";
19
- // =============================================================================
20
- // Request Body Schema
21
- // =============================================================================
22
- const OpenAIResponsesInputText = Schema.Struct({
23
- type: Schema.tag("input_text"),
24
- text: Schema.String,
25
- });
26
- const OpenAIResponsesInputImage = Schema.Struct({
27
- type: Schema.tag("input_image"),
28
- image_url: Schema.String,
29
- });
30
- const OpenAIResponsesInputFile = Schema.Struct({
31
- type: Schema.tag("input_file"),
32
- filename: Schema.String,
33
- file_data: Schema.String,
34
- mime_type: Schema.optional(Schema.String),
35
- });
36
- const OpenAIResponsesInputContent = Schema.Union([
37
- OpenAIResponsesInputText,
38
- OpenAIResponsesInputImage,
39
- OpenAIResponsesInputFile,
40
- ]);
41
- const OpenAIResponsesOutputText = Schema.Struct({
42
- type: Schema.tag("output_text"),
43
- text: Schema.String,
44
- });
45
- const OpenAIResponsesReasoningSummaryText = Schema.Struct({
46
- type: Schema.tag("summary_text"),
47
- text: Schema.String,
48
- });
49
- const OpenAIResponsesReasoningItem = Schema.Struct({
50
- type: Schema.tag("reasoning"),
51
- id: Schema.optionalKey(Schema.String),
52
- summary: Schema.Array(OpenAIResponsesReasoningSummaryText),
53
- encrypted_content: optionalNull(Schema.String),
54
- });
55
- const OpenAIResponsesItemReference = Schema.Struct({
56
- type: Schema.tag("item_reference"),
57
- id: Schema.String,
58
- });
59
- // `function_call_output.output` accepts either a plain string or an ordered
60
- // array of content items so tools can return images and files in addition to text.
61
- // https://platform.openai.com/docs/api-reference/responses/object
62
- const OpenAIResponsesFunctionCallOutputContent = Schema.Union([
63
- OpenAIResponsesInputText,
64
- OpenAIResponsesInputImage,
65
- OpenAIResponsesInputFile,
66
- ]);
67
- const OpenAIResponsesFunctionCallOutput = Schema.Union([
68
- Schema.String,
69
- Schema.Array(OpenAIResponsesFunctionCallOutputContent),
70
- ]);
71
- const OpenAIResponsesInputItem = Schema.Union([
72
- Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
73
- Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
74
- Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
75
- OpenAIResponsesReasoningItem,
76
- OpenAIResponsesItemReference,
77
- Schema.Struct({
78
- type: Schema.tag("function_call"),
79
- call_id: Schema.String,
80
- name: Schema.String,
81
- arguments: Schema.String,
82
- }),
83
- Schema.Struct({
84
- type: Schema.tag("function_call_output"),
85
- call_id: Schema.String,
86
- output: OpenAIResponsesFunctionCallOutput,
87
- }),
88
- ]);
89
- const OpenAIResponsesTool = Schema.Struct({
90
- type: Schema.tag("function"),
91
- name: Schema.String,
92
- description: Schema.String,
93
- parameters: JsonObject,
94
- strict: Schema.optional(Schema.Boolean),
95
- });
16
+ export const PATH = OpenResponses.PATH;
96
17
  const OpenAIResponsesImageGenerationTool = Schema.Struct({
97
18
  type: Schema.tag("image_generation"),
98
19
  action: Schema.optional(Schema.Literals(["auto", "generate", "edit"])),
@@ -104,36 +25,15 @@ const OpenAIResponsesImageGenerationTool = Schema.Struct({
104
25
  quality: Schema.optional(Schema.Literals(["auto", "low", "medium", "high"])),
105
26
  size: Schema.optional(OpenAIImage.Size),
106
27
  });
107
- const OpenAIResponsesTools = Schema.Union([OpenAIResponsesTool, OpenAIResponsesImageGenerationTool]);
28
+ const OpenAIResponsesTools = Schema.Union([OpenResponses.Tool, OpenAIResponsesImageGenerationTool]);
108
29
  const OpenAIResponsesToolChoice = Schema.Union([
109
- Schema.Literals(["auto", "none", "required"]),
110
- Schema.Struct({ type: Schema.tag("function"), name: Schema.String }),
30
+ OpenResponses.ToolChoice,
111
31
  Schema.Struct({ type: Schema.tag("image_generation") }),
112
32
  ]);
113
- // Fields shared between the HTTP body and the WebSocket `response.create`
114
- // message. The HTTP body adds `stream: true`; the WebSocket message adds
115
- // `type: "response.create"`. Defining the shared shape once keeps the two
116
- // transports in sync without a destructure-and-strip dance.
117
33
  const OpenAIResponsesCoreFields = {
118
- model: Schema.String,
119
- input: Schema.Array(OpenAIResponsesInputItem),
120
- instructions: Schema.optional(Schema.String),
34
+ ...OpenResponses.coreFields,
121
35
  tools: optionalArray(OpenAIResponsesTools),
122
36
  tool_choice: Schema.optional(OpenAIResponsesToolChoice),
123
- store: Schema.optional(Schema.Boolean),
124
- service_tier: Schema.optional(OpenAIOptions.OpenAIServiceTier),
125
- prompt_cache_key: Schema.optional(Schema.String),
126
- include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
127
- reasoning: Schema.optional(Schema.Struct({
128
- effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
129
- summary: Schema.optional(Schema.Literal("auto")),
130
- })),
131
- text: Schema.optional(Schema.Struct({
132
- verbosity: Schema.optional(OpenAIOptions.OpenAITextVerbosity),
133
- })),
134
- max_output_tokens: Schema.optional(Schema.Number),
135
- temperature: Schema.optional(Schema.Number),
136
- top_p: Schema.optional(Schema.Number),
137
37
  };
138
38
  const OpenAIResponsesBody = Schema.Struct({
139
39
  ...OpenAIResponsesCoreFields,
@@ -144,69 +44,20 @@ const OpenAIResponsesWebSocketMessage = Schema.StructWithRest(Schema.Struct({
144
44
  ...OpenAIResponsesCoreFields,
145
45
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
146
46
  const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesWebSocketMessage));
147
- const OpenAIResponsesUsage = Schema.Struct({
148
- input_tokens: Schema.optional(Schema.Number),
149
- input_tokens_details: optionalNull(Schema.Struct({ cached_tokens: Schema.optional(Schema.Number) })),
150
- output_tokens: Schema.optional(Schema.Number),
151
- output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })),
152
- total_tokens: Schema.optional(Schema.Number),
153
- });
154
- const OpenAIResponsesStreamItem = Schema.Struct({
155
- type: Schema.String,
156
- id: Schema.optional(Schema.String),
157
- call_id: Schema.optional(Schema.String),
158
- name: Schema.optional(Schema.String),
159
- arguments: Schema.optional(Schema.String),
160
- // Hosted (provider-executed) tool fields. Each hosted tool item carries its
161
- // own subset of these — we capture them generically so we can surface the
162
- // call's typed input portion and round-trip the full result payload without
163
- // hand-rolling a per-tool schema.
164
- status: Schema.optional(Schema.String),
165
- action: Schema.optional(Schema.Unknown),
166
- queries: Schema.optional(Schema.Unknown),
167
- results: Schema.optional(Schema.Unknown),
168
- code: Schema.optional(Schema.String),
169
- container_id: Schema.optional(Schema.String),
170
- outputs: Schema.optional(Schema.Unknown),
171
- server_label: Schema.optional(Schema.String),
172
- output: Schema.optional(Schema.Unknown),
173
- result: Schema.optional(Schema.String),
174
- output_format: Schema.optional(Schema.Literals(["png", "jpeg", "webp"])),
175
- error: Schema.optional(Schema.Unknown),
176
- encrypted_content: optionalNull(Schema.String),
177
- });
178
- // The Responses schema puts streaming error details at the top level and
179
- // response failures under `response.error`. The official SDK also recognizes
180
- // an event-level HTTP-style `error` envelope, so accept all three shapes here.
181
- // https://github.com/openai/openai-openapi/blob/5162af98d3147432c14680df789e8e12d4891e6b/openapi.yaml#L67234-L67382
182
- // https://github.com/openai/openai-node/blob/61539248cbe04665de68a71e6fd878127ae4db87/src/core/streaming.ts#L58-L85
183
- const OpenAIResponsesErrorPayload = Schema.Struct({
184
- code: optionalNull(Schema.String),
185
- message: optionalNull(Schema.String),
186
- param: optionalNull(Schema.String),
187
- });
188
- const OpenAIResponsesEvent = Schema.Struct({
189
- type: Schema.String,
190
- delta: Schema.optional(Schema.String),
191
- item_id: Schema.optional(Schema.String),
192
- summary_index: Schema.optional(Schema.Number),
193
- item: Schema.optional(OpenAIResponsesStreamItem),
194
- response: Schema.optional(Schema.StructWithRest(Schema.Struct({
195
- id: Schema.optional(Schema.String),
196
- service_tier: optionalNull(Schema.String),
197
- incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })),
198
- usage: optionalNull(OpenAIResponsesUsage),
199
- error: optionalNull(OpenAIResponsesErrorPayload),
200
- }), [Schema.Record(Schema.String, Schema.Unknown)])),
201
- code: optionalNull(Schema.String),
202
- message: Schema.optional(Schema.String),
203
- param: optionalNull(Schema.String),
204
- error: optionalNull(OpenAIResponsesErrorPayload),
205
- });
206
- const invalid = ProviderShared.invalidRequest;
207
- // =============================================================================
208
- // Request Lowering
209
- // =============================================================================
47
+ const extension = {
48
+ id: ADAPTER,
49
+ name: NAME,
50
+ lowerMedia: ({ part, media, request }) => {
51
+ if (request.model.provider !== "xai" || media.mime !== "application/pdf")
52
+ return undefined;
53
+ return {
54
+ type: "input_file",
55
+ filename: part.filename ?? "document.pdf",
56
+ file_data: media.base64,
57
+ mime_type: media.mime,
58
+ };
59
+ },
60
+ };
210
61
  const nativeImageToolInput = (tool) => {
211
62
  const native = tool.native?.openai;
212
63
  return ProviderShared.isRecord(native) && native.type === "image_generation" ? native : undefined;
@@ -220,18 +71,11 @@ const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool, inputS
220
71
  if (native !== undefined) {
221
72
  if (Schema.is(OpenAIResponsesImageGenerationTool)(native))
222
73
  return native;
223
- return yield* invalid("OpenAI Responses image generation tool options are invalid");
74
+ return yield* ProviderShared.invalidRequest("OpenAI Responses image generation tool options are invalid");
224
75
  }
225
- return {
226
- type: "function",
227
- name: tool.name,
228
- description: tool.description,
229
- parameters: ToolSchemaProjection.openAI(inputSchema),
230
- // TODO: Read this from OpenAI-specific tool options so direct LLM callers can opt into strict schemas.
231
- strict: false,
232
- };
76
+ return yield* OpenResponses.lowerTool(NAME, tool, inputSchema);
233
77
  });
234
- const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice("OpenAI Responses", toolChoice, {
78
+ const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice(NAME, toolChoice, {
235
79
  auto: () => "auto",
236
80
  none: () => "none",
237
81
  required: () => "required",
@@ -239,274 +83,17 @@ const lowerToolChoice = (toolChoice, tools) => ProviderShared.matchToolChoice("O
239
83
  ? { type: "image_generation" }
240
84
  : { type: "function", name },
241
85
  });
242
- const lowerToolCall = (part) => ({
243
- type: "function_call",
244
- call_id: part.id,
245
- name: part.name,
246
- arguments: ProviderShared.encodeJson(part.input),
247
- });
248
- const lowerReasoning = (part) => {
249
- const openai = part.providerMetadata?.openai;
250
- if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0)
251
- return undefined;
252
- const encryptedContent = typeof openai.reasoningEncryptedContent === "string"
253
- ? openai.reasoningEncryptedContent
254
- : openai.reasoningEncryptedContent === null
255
- ? null
256
- : undefined;
257
- return {
258
- type: "reasoning",
259
- id: openai.itemId,
260
- summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
261
- encrypted_content: encryptedContent,
262
- };
263
- };
264
- const hostedToolItemID = (part) => {
265
- const openai = part.providerMetadata?.openai;
266
- return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0
267
- ? openai.itemId
268
- : undefined;
269
- };
270
- const lowerMedia = Effect.fn("OpenAIResponses.lowerMedia")(function* (part, provider) {
271
- const media = yield* ProviderShared.validateMedia("OpenAI Responses", part, MEDIA_MIMES);
272
- if (media.mime === "application/pdf") {
273
- // xAI models inline bytes and MIME separately; OpenAI uses a data URL in file_data.
274
- if (provider === "xai")
275
- return {
276
- type: "input_file",
277
- filename: part.filename ?? "document.pdf",
278
- file_data: media.base64,
279
- mime_type: media.mime,
280
- };
281
- return {
282
- type: "input_file",
283
- filename: part.filename ?? "document.pdf",
284
- file_data: media.dataUrl,
285
- };
286
- }
287
- return { type: "input_image", image_url: media.dataUrl };
288
- });
289
- const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* (part, provider) {
290
- if (part.type === "text")
291
- return { type: "input_text", text: part.text };
292
- if (part.type === "media")
293
- return yield* lowerMedia(part, provider);
294
- return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]);
295
- });
296
- // Tool results may carry structured text, images, and files. Keep media as provider-native
297
- // content instead of JSON-stringifying base64 into a prompt string.
298
- const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* (item, provider) {
299
- if (item.type === "text")
300
- return { type: "input_text", text: item.text };
301
- return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, provider);
302
- });
303
- const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* (part, provider) {
304
- // Text/json/error results are encoded as a plain string for backward
305
- // compatibility with existing cassettes and provider expectations.
306
- if (part.result.type !== "content")
307
- return ProviderShared.toolResultText(part);
308
- // Preserve the narrowed array element type when compiled through a consumer package.
309
- const content = part.result.value;
310
- return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, provider));
311
- });
312
- const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request) {
313
- const system = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }];
314
- const input = [...system];
315
- const store = OpenAIOptions.store(request);
316
- for (const message of request.messages) {
317
- if (message.role === "system") {
318
- const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message);
319
- const previous = input.at(-1);
320
- if (previous && "role" in previous && previous.role === "user")
321
- input[input.length - 1] = {
322
- role: "user",
323
- content: [...previous.content, { type: "input_text", text: part.text }],
324
- };
325
- else
326
- input.push({ role: "user", content: [{ type: "input_text", text: part.text }] });
327
- continue;
328
- }
329
- if (message.role === "user") {
330
- input.push({
331
- role: "user",
332
- content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.provider)),
333
- });
334
- continue;
335
- }
336
- if (message.role === "assistant") {
337
- const content = [];
338
- const reasoningItems = {};
339
- const reasoningReferences = new Set();
340
- const hostedToolReferences = new Set();
341
- const flushText = () => {
342
- if (content.length === 0)
343
- return;
344
- input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) });
345
- content.splice(0, content.length);
346
- };
347
- for (const part of message.content) {
348
- if (part.type === "text") {
349
- content.push(part);
350
- continue;
351
- }
352
- if (part.type === "reasoning") {
353
- flushText();
354
- const reasoning = lowerReasoning(part);
355
- if (!reasoning)
356
- continue;
357
- if (store !== false) {
358
- if (!reasoningReferences.has(reasoning.id))
359
- input.push({ type: "item_reference", id: reasoning.id });
360
- reasoningReferences.add(reasoning.id);
361
- continue;
362
- }
363
- const existing = reasoningItems[reasoning.id];
364
- if (existing) {
365
- existing.summary.push(...reasoning.summary);
366
- if (typeof reasoning.encrypted_content === "string")
367
- existing.encrypted_content = reasoning.encrypted_content;
368
- continue;
369
- }
370
- const replay = {
371
- type: reasoning.type,
372
- summary: reasoning.summary,
373
- encrypted_content: reasoning.encrypted_content,
374
- };
375
- reasoningItems[reasoning.id] = replay;
376
- input.push(replay);
377
- continue;
378
- }
379
- if (part.type === "tool-call") {
380
- flushText();
381
- if (part.providerExecuted === true)
382
- continue;
383
- input.push(lowerToolCall(part));
384
- continue;
385
- }
386
- if (part.type === "tool-result" && part.providerExecuted === true) {
387
- flushText();
388
- const itemID = hostedToolItemID(part);
389
- if (store !== false && itemID && !hostedToolReferences.has(itemID))
390
- input.push({ type: "item_reference", id: itemID });
391
- if (store === false && part.name === "image_generation" && part.result.type === "content") {
392
- const content = part.result.value;
393
- input.push({
394
- role: "user",
395
- content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request.model.provider)),
396
- });
397
- }
398
- if (itemID)
399
- hostedToolReferences.add(itemID);
400
- continue;
401
- }
402
- return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", [
403
- "text",
404
- "reasoning",
405
- "tool-call",
406
- "tool-result",
407
- ]);
408
- }
409
- flushText();
410
- continue;
411
- }
412
- for (const part of message.content) {
413
- if (!ProviderShared.supportsContent(part, ["tool-result"]))
414
- return yield* ProviderShared.unsupportedContent("OpenAI Responses", "tool", ["tool-result"]);
415
- input.push({
416
- type: "function_call_output",
417
- call_id: part.id,
418
- output: yield* lowerToolResultOutput(part, request.model.provider),
419
- });
420
- }
421
- }
422
- // With store:false, OpenAI only accepts previous reasoning items when the
423
- // complete item has encrypted state. Summary blocks for one item may carry
424
- // that state only on the last block, so filter after they have been joined.
425
- return store === false
426
- ? input.filter((item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string")
427
- : input;
428
- });
429
- const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request) {
430
- const store = OpenAIOptions.store(request);
431
- const promptCacheKey = OpenAIOptions.promptCacheKey(request);
432
- const effort = OpenAIOptions.reasoningEffort(request);
433
- const summary = OpenAIOptions.reasoningSummary(request);
434
- const include = OpenAIOptions.include(request);
435
- const verbosity = OpenAIOptions.textVerbosity(request);
436
- const instructions = OpenAIOptions.instructions(request);
437
- const serviceTier = OpenAIOptions.serviceTier(request);
438
- return {
439
- ...(instructions ? { instructions } : {}),
440
- ...(store !== undefined ? { store } : {}),
441
- ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
442
- ...(include ? { include } : {}),
443
- ...(effort || summary ? { reasoning: { effort, summary } } : {}),
444
- ...(verbosity ? { text: { verbosity } } : {}),
445
- ...(serviceTier ? { service_tier: serviceTier } : {}),
446
- };
447
- });
448
86
  const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request) {
449
- const generation = request.generation;
450
- const options = yield* lowerOptions(request);
87
+ const body = yield* OpenResponses.fromRequest(LLMRequest.update(request, { tools: [], toolChoice: undefined }), extension);
451
88
  const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
452
89
  return {
453
- model: request.model.id,
454
- input: yield* lowerMessages(request),
90
+ ...body,
455
91
  tools: request.tools.length === 0
456
92
  ? undefined
457
93
  : yield* Effect.forEach(request.tools, (tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
458
94
  tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined,
459
- stream: true,
460
- max_output_tokens: generation?.maxTokens,
461
- temperature: generation?.temperature,
462
- top_p: generation?.topP,
463
- ...options,
464
95
  };
465
96
  });
466
- // =============================================================================
467
- // Stream Parsing
468
- // =============================================================================
469
- // OpenAI Responses reports `input_tokens` (inclusive total) with a
470
- // `cached_tokens` subset, and `output_tokens` (inclusive total) with a
471
- // `reasoning_tokens` subset. Pass the totals through and derive the
472
- // non-cached breakdown.
473
- const mapUsage = (usage) => {
474
- if (!usage)
475
- return undefined;
476
- const cached = usage.input_tokens_details?.cached_tokens;
477
- const reasoning = usage.output_tokens_details?.reasoning_tokens;
478
- const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached);
479
- return new Usage({
480
- inputTokens: usage.input_tokens,
481
- outputTokens: usage.output_tokens,
482
- nonCachedInputTokens: nonCached,
483
- cacheReadInputTokens: cached,
484
- reasoningTokens: reasoning,
485
- totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
486
- providerMetadata: { openai: usage },
487
- });
488
- };
489
- const mapFinishReason = (event, hasFunctionCall) => {
490
- const reason = event.response?.incomplete_details?.reason;
491
- if (reason === undefined || reason === null)
492
- return hasFunctionCall ? "tool-calls" : event.type === "response.incomplete" ? "unknown" : "stop";
493
- if (reason === "max_output_tokens")
494
- return "length";
495
- if (reason === "content_filter")
496
- return "content-filter";
497
- return hasFunctionCall ? "tool-calls" : "unknown";
498
- };
499
- const openaiMetadata = (metadata) => ({ openai: metadata });
500
- // Hosted tool items (provider-executed) ship their typed input + status +
501
- // result fields all in one item. We expose them as a `tool-call` +
502
- // `tool-result` pair so consumers can treat them uniformly with client tools,
503
- // only differentiated by `providerExecuted: true`.
504
- //
505
- // One record per OpenAI Responses item type that represents a hosted
506
- // (provider-executed) tool call: the common name we surface, plus an `input`
507
- // extractor that picks the fields the model actually populated for that tool.
508
- // Falling back to `{}` when an entry isn't fully typed keeps unknown tools
509
- // observable without rolling a per-tool schema.
510
97
  const HOSTED_TOOLS = {
511
98
  web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
512
99
  web_search_preview_call: { name: "web_search_preview", input: (item) => item.action ?? {} },
@@ -524,346 +111,53 @@ const HOSTED_TOOLS = {
524
111
  local_shell_call: { name: "local_shell", input: (item) => item.action ?? {} },
525
112
  };
526
113
  const isHostedToolItem = (item) => item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0;
527
- const isReasoningItem = (item) => item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0;
528
- // Round-trip the full item as the structured result so consumers can extract
529
- // outputs / sources / status without re-decoding.
530
114
  const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item) {
531
- const isError = typeof item.error !== "undefined" && item.error !== null;
115
+ const isError = item.error !== undefined && item.error !== null;
532
116
  if (item.type === "image_generation_call" && item.result) {
533
117
  yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError(() => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64")));
118
+ const format = item.output_format ?? "png";
534
119
  return {
535
120
  type: "content",
536
121
  value: [
537
122
  {
538
123
  type: "file",
539
- uri: `data:image/${item.output_format ?? "png"};base64,${item.result}`,
540
- mime: `image/${item.output_format ?? "png"}`,
124
+ uri: `data:image/${format};base64,${item.result}`,
125
+ mime: `image/${format}`,
541
126
  },
542
127
  ],
543
128
  };
544
129
  }
545
130
  return isError ? { type: "error", value: item.error } : { type: "json", value: item };
546
131
  });
547
- const hostedToolEvents = Effect.fn("OpenAIResponses.hostedToolEvents")(function* (item) {
132
+ const onHostedToolDone = Effect.fn("OpenAIResponses.onHostedToolDone")(function* (state, item) {
548
133
  const tool = HOSTED_TOOLS[item.type];
549
- const providerMetadata = openaiMetadata({ itemId: item.id });
550
- return [
551
- LLMEvent.toolCall({
552
- id: item.id,
553
- name: tool.name,
554
- input: tool.input(item),
555
- providerExecuted: true,
556
- providerMetadata,
557
- }),
558
- LLMEvent.toolResult({
559
- id: item.id,
560
- name: tool.name,
561
- result: yield* hostedToolResult(item),
562
- providerExecuted: true,
563
- providerMetadata,
564
- }),
565
- ];
566
- });
567
- const NO_EVENTS = [];
568
- // `response.completed` / `response.incomplete` are clean finishes that emit a
569
- // `finish` event; `response.failed` is a hard failure. All three end the stream,
570
- // so keep this set aligned with `step` and the protocol's terminal predicate.
571
- const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]);
572
- const onOutputTextDelta = (state, event) => {
573
- if (!event.delta)
574
- return [state, NO_EVENTS];
575
- const events = [];
576
- return [
577
- { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) },
578
- events,
579
- ];
580
- };
581
- const onOutputTextDone = (state, event) => {
582
- const events = [];
583
- return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, event.item_id ?? "text-0") }, events];
584
- };
585
- const onReasoningDelta = (state, event) => {
586
- if (!event.delta)
587
- return [state, NO_EVENTS];
588
- const events = [];
589
- const itemID = event.item_id ?? "reasoning-0";
590
- const id = event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID;
591
- return [
592
- {
593
- ...state,
594
- lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
595
- },
596
- events,
597
- ];
598
- };
599
- const onReasoningDone = (state, _event) => [state, NO_EVENTS];
600
- const reasoningMetadata = (item) => openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null });
601
- // OpenAI Responses streams reasoning items in a stable order:
602
- // `output_item.added` (reasoning) →
603
- // `reasoning_summary_part.added` (index=0) →
604
- // `reasoning_summary_text.delta` →
605
- // `reasoning_summary_part.done` (index=0) →
606
- // (repeat for index>0) →
607
- // `output_item.done` (reasoning).
608
- // The handlers below rely on this ordering: `onOutputItemAdded` seeds the
609
- // per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0`
610
- // short-circuits when the entry already exists, and higher-index handlers
611
- // fold against the same entry. Behaviour for out-of-order events is
612
- // best-effort, not guaranteed.
613
- const onOutputItemAdded = (state, event) => {
614
- const item = event.item;
615
- if (item && isReasoningItem(item)) {
616
- const events = [];
617
- return [
618
- {
619
- ...state,
620
- lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)),
621
- reasoningItems: {
622
- ...state.reasoningItems,
623
- [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
624
- },
625
- },
626
- events,
627
- ];
628
- }
629
- if (item?.type !== "function_call" || !item.id)
630
- return [state, NO_EVENTS];
631
- const providerMetadata = openaiMetadata({ itemId: item.id });
134
+ const providerMetadata = OpenResponses.providerMetadata(state, { itemId: item.id });
632
135
  const events = [];
633
136
  const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
634
- return [
635
- {
636
- ...state,
637
- lifecycle,
638
- hasFunctionCall: state.hasFunctionCall,
639
- tools: ToolStream.start(state.tools, item.id, {
640
- id: item.call_id ?? item.id,
641
- name: item.name ?? "",
642
- input: item.arguments ?? "",
643
- providerMetadata,
644
- }),
645
- },
646
- [...events, LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata })],
647
- ];
648
- };
649
- const onReasoningSummaryPartAdded = (state, event) => {
650
- if (!event.item_id || event.summary_index === undefined)
651
- return [state, NO_EVENTS];
652
- const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} };
653
- if (event.summary_index === 0) {
654
- if (state.reasoningItems[event.item_id])
655
- return [state, NO_EVENTS];
656
- const events = [];
657
- return [
658
- {
659
- ...state,
660
- lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${event.item_id}:0`, openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null })),
661
- reasoningItems: {
662
- ...state.reasoningItems,
663
- [event.item_id]: { ...item, summaryParts: { 0: "active" } },
664
- },
665
- },
666
- events,
667
- ];
668
- }
669
- const events = [];
670
- const closed = Object.entries(item.summaryParts)
671
- .filter((entry) => entry[1] === "can-conclude")
672
- .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${event.item_id}:${entry[0]}`, openaiMetadata({ itemId: event.item_id })), state.lifecycle);
673
- return [
674
- {
675
- ...state,
676
- lifecycle: Lifecycle.reasoningStart(closed, events, `${event.item_id}:${event.summary_index}`, openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null })),
677
- reasoningItems: {
678
- ...state.reasoningItems,
679
- [event.item_id]: {
680
- ...item,
681
- summaryParts: {
682
- ...Object.fromEntries(Object.entries(item.summaryParts).map((entry) => entry[1] === "can-conclude" ? [entry[0], "concluded"] : entry)),
683
- [event.summary_index]: "active",
684
- },
685
- },
686
- },
687
- },
688
- events,
689
- ];
690
- };
691
- const onReasoningSummaryPartDone = (state, event) => {
692
- if (!event.item_id || event.summary_index === undefined)
693
- return [state, NO_EVENTS];
694
- const item = state.reasoningItems[event.item_id];
695
- if (!item)
696
- return [state, NO_EVENTS];
697
- const events = [];
698
- return [
699
- {
700
- ...state,
701
- lifecycle: state.store !== false
702
- ? Lifecycle.reasoningEnd(state.lifecycle, events, `${event.item_id}:${event.summary_index}`, openaiMetadata({ itemId: event.item_id }))
703
- : state.lifecycle,
704
- reasoningItems: {
705
- ...state.reasoningItems,
706
- [event.item_id]: {
707
- ...item,
708
- summaryParts: {
709
- ...item.summaryParts,
710
- [event.summary_index]: state.store !== false ? "concluded" : "can-conclude",
711
- },
712
- },
713
- },
714
- },
715
- events,
716
- ];
717
- };
718
- const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* (state, event) {
719
- if (!event.item_id || !event.delta)
720
- return [state, NO_EVENTS];
721
- const result = ToolStream.appendExisting(ADAPTER, state.tools, event.item_id, event.delta, "OpenAI Responses tool argument delta is missing its tool call");
722
- if (ToolStream.isError(result))
723
- return yield* result;
724
- const events = [];
725
- const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
726
- events.push(...result.events);
727
- return [{ ...state, lifecycle, tools: result.tools }, events];
728
- });
729
- const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* (state, event) {
730
- const item = event.item;
731
- if (!item)
732
- return [state, NO_EVENTS];
733
- if (item.type === "message" && item.id)
734
- return onOutputTextDone(state, { ...event, item_id: item.id });
735
- if (item.type === "function_call") {
736
- if (!item.id || !item.call_id || !item.name)
737
- return [state, NO_EVENTS];
738
- const tools = state.tools[item.id]
739
- ? state.tools
740
- : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name });
741
- const result = item.arguments === undefined
742
- ? yield* ToolStream.finish(ADAPTER, tools, item.id)
743
- : yield* ToolStream.finishWithInput(ADAPTER, tools, item.id, item.arguments);
744
- const events = [];
745
- const resultEvents = result.events ?? [];
746
- const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
747
- events.push(...resultEvents);
748
- return [
749
- {
750
- ...state,
751
- lifecycle,
752
- hasFunctionCall: resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) ||
753
- state.hasFunctionCall,
754
- tools: result.tools,
755
- },
756
- events,
757
- ];
758
- }
759
- if (isHostedToolItem(item)) {
760
- const events = [];
761
- const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
762
- events.push(...(yield* hostedToolEvents(item)));
763
- return [{ ...state, lifecycle }, events];
764
- }
765
- if (isReasoningItem(item)) {
766
- const events = [];
767
- const providerMetadata = reasoningMetadata(item);
768
- const reasoningItem = state.reasoningItems[item.id];
769
- if (reasoningItem) {
770
- const lifecycle = Object.entries(reasoningItem.summaryParts)
771
- .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
772
- .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata), state.lifecycle);
773
- const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems;
774
- return [{ ...state, lifecycle, reasoningItems }, events];
775
- }
776
- if (!state.lifecycle.reasoning.has(item.id)) {
777
- const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
778
- events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata }));
779
- events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata }));
780
- return [{ ...state, lifecycle }, events];
781
- }
782
- return [
783
- { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, providerMetadata) },
784
- events,
785
- ];
786
- }
787
- return [state, NO_EVENTS];
788
- });
789
- const onResponseFinish = (state, event) => {
790
- const events = [];
791
- const lifecycle = Lifecycle.finish(state.lifecycle, events, {
792
- reason: {
793
- normalized: mapFinishReason(event, state.hasFunctionCall),
794
- raw: event.response?.incomplete_details?.reason,
795
- },
796
- usage: mapUsage(event.response?.usage),
797
- providerMetadata: event.response?.id || event.response?.service_tier
798
- ? openaiMetadata({
799
- responseId: event.response.id,
800
- serviceTier: event.response.service_tier,
801
- })
802
- : undefined,
803
- });
137
+ events.push(LLMEvent.toolCall({
138
+ id: item.id,
139
+ name: tool.name,
140
+ input: tool.input(item),
141
+ providerExecuted: true,
142
+ providerMetadata,
143
+ }), LLMEvent.toolResult({
144
+ id: item.id,
145
+ name: tool.name,
146
+ result: yield* hostedToolResult(item),
147
+ providerExecuted: true,
148
+ providerMetadata,
149
+ }));
804
150
  return [{ ...state, lifecycle }, events];
805
- };
806
- // Build a single human-readable message from whatever the provider supplied.
807
- // When both code and message are present, prefix the code so consumers see
808
- // the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just
809
- // the bare message — production rate limits and context-length failures used
810
- // to be indistinguishable from generic stream drops.
811
- const providerErrorMessage = (event, fallback) => {
812
- const nested = event.error ?? event.response?.error ?? undefined;
813
- const message = event.message || nested?.message || undefined;
814
- const code = event.code || nested?.code || undefined;
815
- if (message && code)
816
- return `${code}: ${message}`;
817
- return message || code || fallback;
818
- };
819
- const providerError = (event, fallback) => {
820
- const code = event.code || event.error?.code || event.response?.error?.code || undefined;
821
- const message = providerErrorMessage(event, fallback);
822
- return new LLMError({
823
- module: ADAPTER,
824
- method: "stream",
825
- reason: classifyProviderFailure({ message, code }),
826
- });
827
- };
151
+ });
828
152
  const step = (state, event) => {
829
- if (event.type === "response.output_text.delta")
830
- return Effect.succeed(onOutputTextDelta(state, event));
831
- if (event.type === "response.output_text.done")
832
- return Effect.succeed(onOutputTextDone(state, event));
833
- if (event.type === "response.reasoning_text.delta" ||
834
- event.type === "response.reasoning_summary.delta" ||
835
- event.type === "response.reasoning_summary_text.delta")
836
- return Effect.succeed(onReasoningDelta(state, event));
837
- if (event.type === "response.reasoning_text.done" ||
838
- event.type === "response.reasoning_summary.done" ||
839
- event.type === "response.reasoning_summary_text.done")
840
- return Effect.succeed(onReasoningDone(state, event));
841
- if (event.type === "response.reasoning_summary_part.added")
842
- return Effect.succeed(onReasoningSummaryPartAdded(state, event));
843
- if (event.type === "response.reasoning_summary_part.done")
844
- return Effect.succeed(onReasoningSummaryPartDone(state, event));
845
- if (event.type === "response.output_item.added")
846
- return Effect.succeed(onOutputItemAdded(state, event));
847
- if (event.type === "response.function_call_arguments.delta")
848
- return onFunctionCallArgumentsDelta(state, event);
849
- if (event.type === "response.output_item.done")
850
- return onOutputItemDone(state, event);
851
- if (event.type === "response.completed" || event.type === "response.incomplete")
852
- return Effect.succeed(onResponseFinish(state, event));
853
- if (event.type === "response.failed")
854
- return providerError(event, "OpenAI Responses response failed");
855
- if (event.type === "error")
856
- return providerError(event, "OpenAI Responses stream error");
857
- return Effect.succeed([state, NO_EVENTS]);
153
+ if (event.type === "response.reasoning_text.delta" || event.type === "response.reasoning_summary.delta")
154
+ return Effect.succeed(OpenResponses.onReasoningDelta(state, event));
155
+ if (event.type === "response.reasoning_text.done" || event.type === "response.reasoning_summary.done")
156
+ return Effect.succeed(OpenResponses.onReasoningDone(state, event));
157
+ if (event.type === "response.output_item.done" && event.item && isHostedToolItem(event.item))
158
+ return onHostedToolDone(state, event.item);
159
+ return OpenResponses.step(state, event);
858
160
  };
859
- // =============================================================================
860
- // Protocol And OpenAI Route
861
- // =============================================================================
862
- /**
863
- * The OpenAI Responses protocol — request body construction, body schema, and
864
- * the streaming-event state machine. Used by native OpenAI and (once
865
- * registered) Azure OpenAI Responses.
866
- */
867
161
  export const protocol = Protocol.make({
868
162
  id: ADAPTER,
869
163
  body: {
@@ -871,16 +165,10 @@ export const protocol = Protocol.make({
871
165
  from: fromRequest,
872
166
  },
873
167
  stream: {
874
- event: Protocol.jsonEvent(OpenAIResponsesEvent),
875
- initial: (request) => ({
876
- hasFunctionCall: false,
877
- tools: ToolStream.empty(),
878
- lifecycle: Lifecycle.initial(),
879
- reasoningItems: {},
880
- store: OpenAIOptions.store(request),
881
- }),
168
+ event: OpenResponses.protocol.stream.event,
169
+ initial: (request) => OpenResponses.initial(request, extension),
882
170
  step,
883
- terminal: (event) => TERMINAL_TYPES.has(event.type),
171
+ terminal: OpenResponses.terminal,
884
172
  },
885
173
  });
886
174
  const endpoint = Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL });