@effect/ai-openai 4.0.0-beta.7 → 4.0.0-beta.71

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.
Files changed (58) hide show
  1. package/dist/Generated.d.ts +66734 -37723
  2. package/dist/Generated.d.ts.map +1 -1
  3. package/dist/Generated.js +1 -1
  4. package/dist/Generated.js.map +1 -1
  5. package/dist/OpenAiClient.d.ts +167 -27
  6. package/dist/OpenAiClient.d.ts.map +1 -1
  7. package/dist/OpenAiClient.js +337 -44
  8. package/dist/OpenAiClient.js.map +1 -1
  9. package/dist/OpenAiClientGenerated.d.ts +91 -0
  10. package/dist/OpenAiClientGenerated.d.ts.map +1 -0
  11. package/dist/OpenAiClientGenerated.js +84 -0
  12. package/dist/OpenAiClientGenerated.js.map +1 -0
  13. package/dist/OpenAiConfig.d.ts +114 -10
  14. package/dist/OpenAiConfig.d.ts.map +1 -1
  15. package/dist/OpenAiConfig.js +68 -7
  16. package/dist/OpenAiConfig.js.map +1 -1
  17. package/dist/OpenAiEmbeddingModel.d.ts +213 -0
  18. package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
  19. package/dist/OpenAiEmbeddingModel.js +219 -0
  20. package/dist/OpenAiEmbeddingModel.js.map +1 -0
  21. package/dist/OpenAiError.d.ts +168 -35
  22. package/dist/OpenAiError.d.ts.map +1 -1
  23. package/dist/OpenAiError.js +1 -1
  24. package/dist/OpenAiLanguageModel.d.ts +384 -62
  25. package/dist/OpenAiLanguageModel.d.ts.map +1 -1
  26. package/dist/OpenAiLanguageModel.js +416 -166
  27. package/dist/OpenAiLanguageModel.js.map +1 -1
  28. package/dist/OpenAiSchema.d.ts +2298 -0
  29. package/dist/OpenAiSchema.d.ts.map +1 -0
  30. package/dist/OpenAiSchema.js +814 -0
  31. package/dist/OpenAiSchema.js.map +1 -0
  32. package/dist/OpenAiTelemetry.d.ts +59 -18
  33. package/dist/OpenAiTelemetry.d.ts.map +1 -1
  34. package/dist/OpenAiTelemetry.js +35 -8
  35. package/dist/OpenAiTelemetry.js.map +1 -1
  36. package/dist/OpenAiTool.d.ts +157 -62
  37. package/dist/OpenAiTool.d.ts.map +1 -1
  38. package/dist/OpenAiTool.js +134 -39
  39. package/dist/OpenAiTool.js.map +1 -1
  40. package/dist/index.d.ts +19 -33
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +19 -33
  43. package/dist/index.js.map +1 -1
  44. package/dist/internal/errors.js +4 -4
  45. package/dist/internal/errors.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/Generated.ts +9858 -5044
  48. package/src/OpenAiClient.ts +513 -95
  49. package/src/OpenAiClientGenerated.ts +202 -0
  50. package/src/OpenAiConfig.ts +115 -11
  51. package/src/OpenAiEmbeddingModel.ts +357 -0
  52. package/src/OpenAiError.ts +170 -35
  53. package/src/OpenAiLanguageModel.ts +802 -167
  54. package/src/OpenAiSchema.ts +1289 -0
  55. package/src/OpenAiTelemetry.ts +81 -23
  56. package/src/OpenAiTool.ts +135 -40
  57. package/src/index.ts +22 -33
  58. package/src/internal/errors.ts +6 -4
@@ -1,28 +1,60 @@
1
1
  /**
2
- * OpenAI Language Model implementation.
2
+ * The `OpenAiLanguageModel` module provides the OpenAI Responses API
3
+ * implementation of Effect AI's `LanguageModel` service. It translates Effect
4
+ * AI prompts, files, tools, structured output requests, reasoning metadata, and
5
+ * provider options into OpenAI response requests, then converts OpenAI
6
+ * responses and streams back into Effect AI response parts.
3
7
  *
4
- * Provides a LanguageModel implementation for OpenAI's responses API,
5
- * supporting text generation, structured output, tool calling, and streaming.
8
+ * **Mental model**
6
9
  *
7
- * @since 1.0.0
10
+ * `OpenAiClient` owns HTTP transport and provider calls. This module owns
11
+ * protocol translation: request assembly, tool choice conversion, structured
12
+ * output codecs, streaming event handling, response metadata, and GenAI
13
+ * telemetry annotations. {@link model}, {@link layer}, and {@link make} all
14
+ * build the same OpenAI-backed `LanguageModel.LanguageModel` service from a
15
+ * model id and optional request defaults.
16
+ *
17
+ * **Common tasks**
18
+ *
19
+ * - Provide an OpenAI-backed `LanguageModel.LanguageModel` from an existing
20
+ * `OpenAiClient`
21
+ * - Generate text or stream text through Effect AI's provider-neutral language
22
+ * model API
23
+ * - Use OpenAI provider metadata for files, reasoning items, tool calls, and
24
+ * response parts
25
+ * - Scope Responses API defaults with {@link Config} and
26
+ * {@link withConfigOverride}
27
+ *
28
+ * **Gotchas**
29
+ *
30
+ * - Some OpenAI model families receive system instructions as developer
31
+ * messages because the Responses API treats them differently.
32
+ * - File prompt parts are sent either as provider file ids or as base64
33
+ * content, depending on `fileIdPrefixes`.
34
+ * - Structured output and tool-call behavior depends on both Effect AI tool
35
+ * definitions and OpenAI model capabilities.
36
+ *
37
+ * @since 4.0.0
8
38
  */
39
+ import * as Context from "effect/Context"
9
40
  import * as DateTime from "effect/DateTime"
10
41
  import * as Effect from "effect/Effect"
11
42
  import * as Encoding from "effect/Encoding"
12
43
  import { dual } from "effect/Function"
13
44
  import * as Layer from "effect/Layer"
45
+ import * as Option from "effect/Option"
14
46
  import * as Predicate from "effect/Predicate"
15
47
  import * as Redactable from "effect/Redactable"
16
48
  import * as Schema from "effect/Schema"
17
49
  import * as AST from "effect/SchemaAST"
18
- import * as ServiceMap from "effect/ServiceMap"
19
50
  import * as Stream from "effect/Stream"
20
51
  import type { Span } from "effect/Tracer"
21
- import type { DeepMutable, Simplify } from "effect/Types"
52
+ import type { DeepMutable, Mutable, Simplify } from "effect/Types"
22
53
  import * as AiError from "effect/unstable/ai/AiError"
23
54
  import * as IdGenerator from "effect/unstable/ai/IdGenerator"
24
55
  import * as LanguageModel from "effect/unstable/ai/LanguageModel"
25
56
  import * as AiModel from "effect/unstable/ai/Model"
57
+ import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput"
26
58
  import type * as Prompt from "effect/unstable/ai/Prompt"
27
59
  import type * as Response from "effect/unstable/ai/Response"
28
60
  import * as Tool from "effect/unstable/ai/Tool"
@@ -31,6 +63,7 @@ import type * as HttpClientResponse from "effect/unstable/http/HttpClientRespons
31
63
  import * as Generated from "./Generated.ts"
32
64
  import * as InternalUtilities from "./internal/utilities.ts"
33
65
  import { OpenAiClient } from "./OpenAiClient.ts"
66
+ import type * as OpenAiSchema from "./OpenAiSchema.ts"
34
67
  import { addGenAIAnnotations } from "./OpenAiTelemetry.ts"
35
68
  import type * as OpenAiTool from "./OpenAiTool.ts"
36
69
 
@@ -38,8 +71,10 @@ const ResponseModelIds = Generated.ModelIdsResponses.members[1]
38
71
  const SharedModelIds = Generated.ModelIdsShared.members[1]
39
72
 
40
73
  /**
41
- * @since 1.0.0
74
+ * OpenAI model identifiers supported by the Responses API language model.
75
+ *
42
76
  * @category models
77
+ * @since 4.0.0
43
78
  */
44
79
  export type Model = typeof ResponseModelIds.Encoded | typeof SharedModelIds.Encoded
45
80
 
@@ -55,15 +90,27 @@ type ImageDetail = "auto" | "low" | "high"
55
90
  /**
56
91
  * Service definition for OpenAI language model configuration.
57
92
  *
58
- * @since 1.0.0
93
+ * **When to use**
94
+ *
95
+ * Use when you need to provide OpenAI Responses API request defaults through
96
+ * Effect context for language model operations.
97
+ *
98
+ * **Details**
99
+ *
100
+ * Config values are merged with the config object passed to `model`, `make`, or
101
+ * `layer`, with scoped context values taking precedence.
102
+ *
103
+ * @see {@link withConfigOverride} for scoping language model request overrides
104
+ *
59
105
  * @category services
106
+ * @since 4.0.0
60
107
  */
61
- export class Config extends ServiceMap.Service<
108
+ export class Config extends Context.Service<
62
109
  Config,
63
110
  Simplify<
64
111
  & Partial<
65
112
  Omit<
66
- typeof Generated.CreateResponse.Encoded,
113
+ typeof OpenAiSchema.CreateResponse.Encoded,
67
114
  "input" | "tools" | "tool_choice" | "stream" | "text"
68
115
  >
69
116
  >
@@ -105,7 +152,16 @@ export class Config extends ServiceMap.Service<
105
152
  // =============================================================================
106
153
 
107
154
  declare module "effect/unstable/ai/Prompt" {
155
+ /**
156
+ * OpenAI-specific options for file prompt parts.
157
+ *
158
+ * @category request
159
+ * @since 4.0.0
160
+ */
108
161
  export interface FilePartOptions extends ProviderOptions {
162
+ /**
163
+ * Provider-specific file options for the OpenAI Responses API.
164
+ */
109
165
  readonly openai?: {
110
166
  /**
111
167
  * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`.
@@ -114,7 +170,16 @@ declare module "effect/unstable/ai/Prompt" {
114
170
  } | null
115
171
  }
116
172
 
173
+ /**
174
+ * OpenAI-specific options for reasoning prompt parts.
175
+ *
176
+ * @category request
177
+ * @since 4.0.0
178
+ */
117
179
  export interface ReasoningPartOptions extends ProviderOptions {
180
+ /**
181
+ * Provider-specific reasoning options for the OpenAI Responses API.
182
+ */
118
183
  readonly openai?: {
119
184
  /**
120
185
  * The ID of the item to reference.
@@ -129,7 +194,16 @@ declare module "effect/unstable/ai/Prompt" {
129
194
  } | null
130
195
  }
131
196
 
197
+ /**
198
+ * OpenAI-specific options for assistant tool-call prompt parts.
199
+ *
200
+ * @category request
201
+ * @since 4.0.0
202
+ */
132
203
  export interface ToolCallPartOptions extends ProviderOptions {
204
+ /**
205
+ * Provider-specific tool-call options for the OpenAI Responses API.
206
+ */
133
207
  readonly openai?: {
134
208
  /**
135
209
  * The ID of the item to reference.
@@ -138,7 +212,7 @@ declare module "effect/unstable/ai/Prompt" {
138
212
  /**
139
213
  * The status of item.
140
214
  */
141
- readonly status?: typeof Generated.Message.Encoded["status"] | null
215
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
142
216
  /**
143
217
  * The ID of the approval request.
144
218
  */
@@ -146,7 +220,16 @@ declare module "effect/unstable/ai/Prompt" {
146
220
  } | null
147
221
  }
148
222
 
223
+ /**
224
+ * OpenAI-specific options for tool-result prompt parts.
225
+ *
226
+ * @category request
227
+ * @since 4.0.0
228
+ */
149
229
  export interface ToolResultPartOptions extends ProviderOptions {
230
+ /**
231
+ * Provider-specific tool-result options for the OpenAI Responses API.
232
+ */
150
233
  readonly openai?: {
151
234
  /**
152
235
  * The ID of the item to reference.
@@ -155,7 +238,7 @@ declare module "effect/unstable/ai/Prompt" {
155
238
  /**
156
239
  * The status of item.
157
240
  */
158
- readonly status?: typeof Generated.Message.Encoded["status"] | null
241
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
159
242
  /**
160
243
  * The ID of the approval request.
161
244
  */
@@ -163,7 +246,16 @@ declare module "effect/unstable/ai/Prompt" {
163
246
  } | null
164
247
  }
165
248
 
249
+ /**
250
+ * OpenAI-specific options for text prompt parts.
251
+ *
252
+ * @category request
253
+ * @since 4.0.0
254
+ */
166
255
  export interface TextPartOptions extends ProviderOptions {
256
+ /**
257
+ * Provider-specific text options for the OpenAI Responses API.
258
+ */
167
259
  readonly openai?: {
168
260
  /**
169
261
  * The ID of the item to reference.
@@ -172,18 +264,30 @@ declare module "effect/unstable/ai/Prompt" {
172
264
  /**
173
265
  * The status of item.
174
266
  */
175
- readonly status?: typeof Generated.Message.Encoded["status"] | null
267
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
176
268
  /**
177
269
  * A list of annotations that apply to the output text.
178
270
  */
179
- readonly annotations?: ReadonlyArray<typeof Generated.Annotation.Encoded> | null
271
+ readonly annotations?: ReadonlyArray<typeof OpenAiSchema.Annotation.Encoded> | null
180
272
  } | null
181
273
  }
182
274
  }
183
275
 
184
276
  declare module "effect/unstable/ai/Response" {
277
+ /**
278
+ * OpenAI metadata attached to a complete text response part.
279
+ *
280
+ * @category response
281
+ * @since 4.0.0
282
+ */
185
283
  export interface TextPartMetadata extends ProviderMetadata {
284
+ /**
285
+ * Provider-specific metadata returned for the text part.
286
+ */
186
287
  readonly openai?: {
288
+ /**
289
+ * The OpenAI item ID associated with the text part.
290
+ */
187
291
  readonly itemId?: string | null
188
292
  /**
189
293
  * If the model emits a refusal content part, the refusal explanation
@@ -194,63 +298,171 @@ declare module "effect/unstable/ai/Response" {
194
298
  /**
195
299
  * The status of item.
196
300
  */
197
- readonly status?: typeof Generated.Message.Encoded["status"] | null
301
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
198
302
  /**
199
303
  * The text content part annotations.
200
304
  */
201
- readonly annotations?: ReadonlyArray<typeof Generated.Annotation.Encoded> | null
305
+ readonly annotations?: ReadonlyArray<typeof OpenAiSchema.Annotation.Encoded> | null
202
306
  }
203
307
  }
204
308
 
309
+ /**
310
+ * OpenAI metadata emitted when a streamed text part starts.
311
+ *
312
+ * @category response
313
+ * @since 4.0.0
314
+ */
205
315
  export interface TextStartPartMetadata extends ProviderMetadata {
316
+ /**
317
+ * Provider-specific metadata returned for the streamed text start.
318
+ */
206
319
  readonly openai?: {
320
+ /**
321
+ * The OpenAI item ID associated with the streamed text part.
322
+ */
207
323
  readonly itemId?: string | null
208
324
  } | null
209
325
  }
210
326
 
327
+ /**
328
+ * OpenAI metadata emitted when a streamed text part ends.
329
+ *
330
+ * @category response
331
+ * @since 4.0.0
332
+ */
211
333
  export interface TextEndPartMetadata extends ProviderMetadata {
334
+ /**
335
+ * Provider-specific metadata returned for the streamed text end.
336
+ */
212
337
  readonly openai?: {
338
+ /**
339
+ * The OpenAI item ID associated with the streamed text part.
340
+ */
213
341
  readonly itemId?: string | null
214
- readonly annotations?: ReadonlyArray<typeof Generated.Annotation.Encoded> | null
342
+ /**
343
+ * The annotations collected for the completed streamed text part.
344
+ */
345
+ readonly annotations?: ReadonlyArray<typeof OpenAiSchema.Annotation.Encoded> | null
215
346
  } | null
216
347
  }
217
348
 
349
+ /**
350
+ * OpenAI metadata attached to a complete reasoning response part.
351
+ *
352
+ * @category response
353
+ * @since 4.0.0
354
+ */
218
355
  export interface ReasoningPartMetadata extends ProviderMetadata {
356
+ /**
357
+ * Provider-specific metadata returned for the reasoning part.
358
+ */
219
359
  readonly openai?: {
360
+ /**
361
+ * The OpenAI item ID associated with the reasoning part.
362
+ */
220
363
  readonly itemId?: string | null
364
+ /**
365
+ * Encrypted reasoning content that can be sent back in later requests.
366
+ */
221
367
  readonly encryptedContent?: string | null
222
368
  } | null
223
369
  }
224
370
 
371
+ /**
372
+ * OpenAI metadata emitted when a streamed reasoning part starts.
373
+ *
374
+ * @category response
375
+ * @since 4.0.0
376
+ */
225
377
  export interface ReasoningStartPartMetadata extends ProviderMetadata {
378
+ /**
379
+ * Provider-specific metadata returned for the streamed reasoning start.
380
+ */
226
381
  readonly openai?: {
382
+ /**
383
+ * The OpenAI item ID associated with the reasoning part.
384
+ */
227
385
  readonly itemId?: string | null
386
+ /**
387
+ * Encrypted reasoning content that can be sent back in later requests.
388
+ */
228
389
  readonly encryptedContent?: string | null
229
390
  } | null
230
391
  }
231
392
 
393
+ /**
394
+ * OpenAI metadata emitted for a streamed reasoning delta.
395
+ *
396
+ * @category response
397
+ * @since 4.0.0
398
+ */
232
399
  export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
400
+ /**
401
+ * Provider-specific metadata returned for the streamed reasoning delta.
402
+ */
233
403
  readonly openai?: {
404
+ /**
405
+ * The OpenAI item ID associated with the reasoning part.
406
+ */
234
407
  readonly itemId?: string | null
235
408
  } | null
236
409
  }
237
410
 
411
+ /**
412
+ * OpenAI metadata emitted when a streamed reasoning part ends.
413
+ *
414
+ * @category response
415
+ * @since 4.0.0
416
+ */
238
417
  export interface ReasoningEndPartMetadata extends ProviderMetadata {
418
+ /**
419
+ * Provider-specific metadata returned for the streamed reasoning end.
420
+ */
239
421
  readonly openai?: {
422
+ /**
423
+ * The OpenAI item ID associated with the reasoning part.
424
+ */
240
425
  readonly itemId?: string | null
426
+ /**
427
+ * Encrypted reasoning content that can be sent back in later requests.
428
+ */
241
429
  readonly encryptedContent?: string
242
430
  } | null
243
431
  }
244
432
 
433
+ /**
434
+ * OpenAI metadata attached to tool-call response parts.
435
+ *
436
+ * @category response
437
+ * @since 4.0.0
438
+ */
245
439
  export interface ToolCallPartMetadata extends ProviderMetadata {
440
+ /**
441
+ * Provider-specific metadata returned for the tool call.
442
+ */
246
443
  readonly openai?: {
444
+ /**
445
+ * The OpenAI item ID associated with the tool call.
446
+ */
247
447
  readonly itemId?: string | null
248
448
  } | null
249
449
  }
250
450
 
451
+ /**
452
+ * OpenAI metadata attached to document source citations.
453
+ *
454
+ * @category response
455
+ * @since 4.0.0
456
+ */
251
457
  export interface DocumentSourcePartMetadata extends ProviderMetadata {
458
+ /**
459
+ * Provider-specific citation metadata for the OpenAI Responses API.
460
+ */
252
461
  readonly openai?:
253
462
  | {
463
+ /**
464
+ * Identifies a citation to an uploaded file.
465
+ */
254
466
  readonly type: "file_citation"
255
467
  /**
256
468
  * The index of the file in the list of files.
@@ -262,6 +474,9 @@ declare module "effect/unstable/ai/Response" {
262
474
  readonly fileId: string
263
475
  }
264
476
  | {
477
+ /**
478
+ * Identifies a citation to a generated file path.
479
+ */
265
480
  readonly type: "file_path"
266
481
  /**
267
482
  * The index of the file in the list of files.
@@ -273,6 +488,9 @@ declare module "effect/unstable/ai/Response" {
273
488
  readonly fileId: string
274
489
  }
275
490
  | {
491
+ /**
492
+ * Identifies a citation to a file inside a container.
493
+ */
276
494
  readonly type: "container_file_citation"
277
495
  /**
278
496
  * The ID of the file.
@@ -286,8 +504,20 @@ declare module "effect/unstable/ai/Response" {
286
504
  | null
287
505
  }
288
506
 
507
+ /**
508
+ * OpenAI metadata attached to URL source citations.
509
+ *
510
+ * @category response
511
+ * @since 4.0.0
512
+ */
289
513
  export interface UrlSourcePartMetadata extends ProviderMetadata {
514
+ /**
515
+ * Provider-specific URL citation metadata for the OpenAI Responses API.
516
+ */
290
517
  readonly openai?: {
518
+ /**
519
+ * Identifies a citation to a URL.
520
+ */
291
521
  readonly type: "url_citation"
292
522
  /**
293
523
  * The index of the first character of the URL citation in the message.
@@ -300,8 +530,20 @@ declare module "effect/unstable/ai/Response" {
300
530
  } | null
301
531
  }
302
532
 
533
+ /**
534
+ * OpenAI metadata attached to finish response parts.
535
+ *
536
+ * @category response
537
+ * @since 4.0.0
538
+ */
303
539
  export interface FinishPartMetadata extends ProviderMetadata {
540
+ /**
541
+ * Provider-specific metadata returned when generation finishes.
542
+ */
304
543
  readonly openai?: {
544
+ /**
545
+ * The service tier reported by OpenAI for the response.
546
+ */
305
547
  readonly serviceTier?: "default" | "auto" | "flex" | "scale" | "priority" | null
306
548
  } | null
307
549
  }
@@ -312,31 +554,58 @@ declare module "effect/unstable/ai/Response" {
312
554
  // =============================================================================
313
555
 
314
556
  /**
315
- * @since 1.0.0
557
+ * Creates an OpenAI model descriptor that can be provided with
558
+ * `Effect.provide`.
559
+ *
560
+ * **When to use**
561
+ *
562
+ * Use when you want an OpenAI language model value that carries provider and
563
+ * model metadata and can be supplied directly to an Effect program.
564
+ *
565
+ * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly
566
+ * @see {@link make} for constructing the language model service effectfully
567
+ *
316
568
  * @category constructors
569
+ * @since 4.0.0
317
570
  */
318
571
  export const model = (
319
572
  model: (string & {}) | Model,
320
573
  config?: Omit<typeof Config.Service, "model">
321
574
  ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> =>
322
- AiModel.make("openai", layer({ model, config }))
575
+ AiModel.make("openai", model, layer({ model, config }))
323
576
 
324
577
  // TODO
325
578
  // /**
326
- // * @since 1.0.0
579
+ // * @since 4.0.0
327
580
  // * @category constructors
328
581
  // */
329
582
  // export const modelWithTokenizer = (
330
583
  // model: (string & {}) | Model,
331
584
  // config?: Omit<typeof Config.Service, "model">
332
585
  // ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
333
- // AiModel.make("openai", layerWithTokenizer({ model, config }))
586
+ // AiModel.make("openai", model, layerWithTokenizer({ model, config }))
334
587
 
335
588
  /**
336
- * Creates an OpenAI language model service.
589
+ * Creates an OpenAI `LanguageModel` service from a model identifier and
590
+ * optional request defaults.
591
+ *
592
+ * **When to use**
593
+ *
594
+ * Use when an Effect needs to construct a `LanguageModel.Service` value backed
595
+ * by `OpenAiClient`.
596
+ *
597
+ * **Details**
598
+ *
599
+ * The returned effect requires `OpenAiClient`. Request defaults from the
600
+ * `config` option are merged with any `Config` service in the context, with
601
+ * context values taking precedence. The service supports both `generateText`
602
+ * and `streamText`.
603
+ *
604
+ * @see {@link layer} for providing the service as a `Layer`
605
+ * @see {@link model} for creating a model descriptor for `Effect.provide`
337
606
  *
338
- * @since 1.0.0
339
607
  * @category constructors
608
+ * @since 4.0.0
340
609
  */
341
610
  export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
342
611
  readonly model: (string & {}) | Model
@@ -345,7 +614,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
345
614
  const client = yield* OpenAiClient
346
615
 
347
616
  const makeConfig = Effect.gen(function*() {
348
- const services = yield* Effect.services<never>()
617
+ const services = yield* Effect.context<never>()
349
618
  return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
350
619
  })
351
620
 
@@ -354,9 +623,9 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
354
623
  readonly config: typeof Config.Service
355
624
  readonly options: LanguageModel.ProviderOptions
356
625
  readonly toolNameMapper: Tool.NameMapper<Tools>
357
- }): Effect.fn.Return<typeof Generated.CreateResponse.Encoded, AiError.AiError> {
358
- const include = new Set<typeof Generated.IncludeEnum.Encoded>()
359
- const capabilities = getModelCapabilities(config.model!)
626
+ }): Effect.fn.Return<typeof OpenAiSchema.CreateResponse.Encoded, AiError.AiError> {
627
+ const include = new Set<typeof OpenAiSchema.IncludeEnum.Encoded>()
628
+ const capabilities = getModelCapabilities(config.model as string)
360
629
  const messages = yield* prepareMessages({
361
630
  config,
362
631
  options,
@@ -369,26 +638,29 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
369
638
  options,
370
639
  toolNameMapper
371
640
  })
372
- const responseFormat = prepareResponseFormat({
641
+ const responseFormat = yield* prepareResponseFormat({
373
642
  config,
374
643
  options
375
644
  })
376
- const request: typeof Generated.CreateResponse.Encoded = {
377
- ...config,
645
+ const { fileIdPrefixes: _fip, strictJsonSchema: _sjs, ...apiConfig } = config
646
+ const request: Mutable<typeof OpenAiSchema.CreateResponse.Encoded> = {
647
+ ...apiConfig,
378
648
  input: messages,
379
- include: include.size > 0 ? Array.from(include) : null,
649
+ include: include.size > 0 ? Array.from(include) : undefined,
380
650
  text: {
381
- verbosity: config.text?.verbosity ?? null,
651
+ verbosity: config.text?.verbosity ?? undefined,
382
652
  format: responseFormat
383
- },
384
- ...(Predicate.isNotUndefined(tools) ? { tools } : undefined),
385
- ...(Predicate.isNotUndefined(toolChoice) ? { tool_choice: toolChoice } : undefined)
653
+ }
386
654
  }
655
+ if (tools) request.tools = tools
656
+ if (toolChoice) request.tool_choice = toolChoice
657
+ if (options.previousResponseId) request.previous_response_id = options.previousResponseId
387
658
  return request
388
659
  }
389
660
  )
390
661
 
391
662
  return yield* LanguageModel.make({
663
+ codecTransformer: toCodecOpenAI,
392
664
  generateText: Effect.fnUntraced(
393
665
  function*(options) {
394
666
  const config = yield* makeConfig
@@ -433,10 +705,27 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
433
705
  })
434
706
 
435
707
  /**
436
- * Creates a layer for the OpenAI language model.
708
+ * Creates a layer that provides the OpenAI `LanguageModel.LanguageModel`
709
+ * service.
710
+ *
711
+ * **When to use**
712
+ *
713
+ * Use when composing application layers and you want OpenAI to satisfy
714
+ * `LanguageModel.LanguageModel` while supplying `OpenAiClient` from another
715
+ * layer.
716
+ *
717
+ * **Details**
718
+ *
719
+ * The `config` option supplies request defaults for the selected model. Scoped
720
+ * values from `withConfigOverride` are merged when each request is built and
721
+ * take precedence over these defaults.
722
+ *
723
+ * @see {@link make} for constructing the language model service effectfully
724
+ * @see {@link model} for creating a model descriptor for `Effect.provide`
725
+ * @see {@link withConfigOverride} for scoped request configuration overrides
437
726
  *
438
- * @since 1.0.0
439
727
  * @category layers
728
+ * @since 4.0.0
440
729
  */
441
730
  export const layer = (options: {
442
731
  readonly model: (string & {}) | Model
@@ -445,39 +734,114 @@ export const layer = (options: {
445
734
  Layer.effect(LanguageModel.LanguageModel, make(options))
446
735
 
447
736
  /**
448
- * Provides config overrides for OpenAI language model operations.
737
+ * Provides scoped config overrides for OpenAI language model operations.
738
+ *
739
+ * **When to use**
740
+ *
741
+ * Use to apply OpenAI Responses API config overrides around one or more
742
+ * language model operations without changing the defaults passed to `model`,
743
+ * `make`, or `layer`.
744
+ *
745
+ * **Details**
746
+ *
747
+ * The override is dual, so it can be used in pipe form or as
748
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
749
+ * existing `Config` service in the current context, and the override values take
750
+ * precedence.
751
+ *
752
+ * @see {@link Config} for the scoped configuration service consumed by this function
449
753
  *
450
- * @since 1.0.0
451
754
  * @category configuration
755
+ * @since 4.0.0
452
756
  */
453
757
  export const withConfigOverride: {
454
758
  /**
455
- * Provides config overrides for OpenAI language model operations.
759
+ * Provides scoped config overrides for OpenAI language model operations.
760
+ *
761
+ * **When to use**
762
+ *
763
+ * Use to apply OpenAI Responses API config overrides around one or more
764
+ * language model operations without changing the defaults passed to `model`,
765
+ * `make`, or `layer`.
766
+ *
767
+ * **Details**
768
+ *
769
+ * The override is dual, so it can be used in pipe form or as
770
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
771
+ * existing `Config` service in the current context, and the override values take
772
+ * precedence.
773
+ *
774
+ * @see {@link Config} for the scoped configuration service consumed by this function
456
775
  *
457
- * @since 1.0.0
458
776
  * @category configuration
777
+ * @since 4.0.0
459
778
  */
460
779
  (overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
461
780
  /**
462
- * Provides config overrides for OpenAI language model operations.
781
+ * Provides scoped config overrides for OpenAI language model operations.
782
+ *
783
+ * **When to use**
784
+ *
785
+ * Use to apply OpenAI Responses API config overrides around one or more
786
+ * language model operations without changing the defaults passed to `model`,
787
+ * `make`, or `layer`.
788
+ *
789
+ * **Details**
790
+ *
791
+ * The override is dual, so it can be used in pipe form or as
792
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
793
+ * existing `Config` service in the current context, and the override values take
794
+ * precedence.
795
+ *
796
+ * @see {@link Config} for the scoped configuration service consumed by this function
463
797
  *
464
- * @since 1.0.0
465
798
  * @category configuration
799
+ * @since 4.0.0
466
800
  */
467
801
  <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
468
802
  } = dual<
469
803
  /**
470
- * Provides config overrides for OpenAI language model operations.
804
+ * Provides scoped config overrides for OpenAI language model operations.
805
+ *
806
+ * **When to use**
807
+ *
808
+ * Use to apply OpenAI Responses API config overrides around one or more
809
+ * language model operations without changing the defaults passed to `model`,
810
+ * `make`, or `layer`.
811
+ *
812
+ * **Details**
813
+ *
814
+ * The override is dual, so it can be used in pipe form or as
815
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
816
+ * existing `Config` service in the current context, and the override values take
817
+ * precedence.
818
+ *
819
+ * @see {@link Config} for the scoped configuration service consumed by this function
471
820
  *
472
- * @since 1.0.0
473
821
  * @category configuration
822
+ * @since 4.0.0
474
823
  */
475
824
  (overrides: typeof Config.Service) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
476
825
  /**
477
- * Provides config overrides for OpenAI language model operations.
826
+ * Provides scoped config overrides for OpenAI language model operations.
827
+ *
828
+ * **When to use**
829
+ *
830
+ * Use to apply OpenAI Responses API config overrides around one or more
831
+ * language model operations without changing the defaults passed to `model`,
832
+ * `make`, or `layer`.
833
+ *
834
+ * **Details**
835
+ *
836
+ * The override is dual, so it can be used in pipe form or as
837
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
838
+ * existing `Config` service in the current context, and the override values take
839
+ * precedence.
840
+ *
841
+ * @see {@link Config} for the scoped configuration service consumed by this function
478
842
  *
479
- * @since 1.0.0
480
843
  * @category configuration
844
+ * @since 4.0.0
481
845
  */
482
846
  <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
483
847
  >(2, (self, overrides) =>
@@ -512,10 +876,10 @@ const prepareMessages = Effect.fnUntraced(
512
876
  }: {
513
877
  readonly config: typeof Config.Service
514
878
  readonly options: LanguageModel.ProviderOptions
515
- readonly include: Set<typeof Generated.IncludeEnum.Encoded>
879
+ readonly include: Set<typeof OpenAiSchema.IncludeEnum.Encoded>
516
880
  readonly capabilities: ModelCapabilities
517
881
  readonly toolNameMapper: Tool.NameMapper<Tools>
518
- }): Effect.fn.Return<ReadonlyArray<typeof Generated.InputItem.Encoded>, AiError.AiError> {
882
+ }): Effect.fn.Return<ReadonlyArray<typeof OpenAiSchema.InputItem.Encoded>, AiError.AiError> {
519
883
  const processedApprovalIds = new Set<string>()
520
884
 
521
885
  const hasConversation = Predicate.isNotNullish(config.conversation)
@@ -547,27 +911,28 @@ const prepareMessages = Effect.fnUntraced(
547
911
  if (config.store === false && capabilities.isReasoningModel) {
548
912
  include.add("reasoning.encrypted_content")
549
913
  }
550
- if (Predicate.isNotUndefined(codeInterpreterTool)) {
914
+ if (codeInterpreterTool) {
551
915
  include.add("code_interpreter_call.outputs")
552
916
  }
553
- if (Predicate.isNotUndefined(webSearchTool) || Predicate.isNotUndefined(webSearchPreviewTool)) {
917
+ if (webSearchTool || webSearchPreviewTool) {
554
918
  include.add("web_search_call.action.sources")
555
919
  }
556
920
 
557
- const messages: Array<typeof Generated.InputItem.Encoded> = []
921
+ const messages: Array<typeof OpenAiSchema.InputItem.Encoded> = []
922
+ const prompt = options.incrementalPrompt ?? options.prompt
558
923
 
559
- for (const message of options.prompt.content) {
924
+ for (const message of prompt.content) {
560
925
  switch (message.role) {
561
926
  case "system": {
562
927
  messages.push({
563
- role: getSystemMessageMode(config.model!),
928
+ role: getSystemMessageMode(config.model as string),
564
929
  content: message.content
565
930
  })
566
931
  break
567
932
  }
568
933
 
569
934
  case "user": {
570
- const content: Array<typeof Generated.InputContent.Encoded> = []
935
+ const content: Array<typeof OpenAiSchema.InputContent.Encoded> = []
571
936
 
572
937
  for (let index = 0; index < message.content.length; index++) {
573
938
  const part = message.content[index]
@@ -630,7 +995,7 @@ const prepareMessages = Effect.fnUntraced(
630
995
  }
631
996
 
632
997
  case "assistant": {
633
- const reasoningMessages: Record<string, DeepMutable<typeof Generated.ReasoningItem.Encoded>> = {}
998
+ const reasoningMessages: Record<string, DeepMutable<typeof OpenAiSchema.ReasoningItem.Encoded>> = {}
634
999
 
635
1000
  for (const part of message.content) {
636
1001
  switch (part.type) {
@@ -689,7 +1054,7 @@ const prepareMessages = Effect.fnUntraced(
689
1054
  }
690
1055
  }
691
1056
  } else {
692
- const summaryParts: Array<typeof Generated.SummaryTextContent.Encoded> = []
1057
+ const summaryParts: Array<typeof OpenAiSchema.SummaryTextContent.Encoded> = []
693
1058
 
694
1059
  if (part.text.length > 0) {
695
1060
  summaryParts.push({ type: "summary_text", text: part.text })
@@ -700,7 +1065,9 @@ const prepareMessages = Effect.fnUntraced(
700
1065
  type: "reasoning",
701
1066
  id,
702
1067
  summary: summaryParts,
703
- encrypted_content: encryptedContent ?? null
1068
+ ...(Predicate.isNotNull(encryptedContent)
1069
+ ? { encrypted_content: encryptedContent }
1070
+ : undefined)
704
1071
  }
705
1072
 
706
1073
  messages.push(reasoningMessages[id])
@@ -921,7 +1288,7 @@ const buildHttpRequestDetails = (
921
1288
  method: request.method,
922
1289
  url: request.url,
923
1290
  urlParams: Array.from(request.urlParams),
924
- hash: request.hash,
1291
+ hash: Option.getOrUndefined(request.hash),
925
1292
  headers: Redactable.redact(request.headers) as Record<string, string>
926
1293
  })
927
1294
 
@@ -936,7 +1303,56 @@ const buildHttpResponseDetails = (
936
1303
  // Response Conversion
937
1304
  // =============================================================================
938
1305
 
939
- type ResponseStreamEvent = typeof Generated.ResponseStreamEvent.Type
1306
+ type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type
1307
+
1308
+ type KnownResponseStreamEventType =
1309
+ | "response.created"
1310
+ | "response.completed"
1311
+ | "response.incomplete"
1312
+ | "response.failed"
1313
+ | "response.output_item.added"
1314
+ | "response.output_item.done"
1315
+ | "response.output_text.delta"
1316
+ | "response.output_text.annotation.added"
1317
+ | "response.reasoning_summary_part.added"
1318
+ | "response.reasoning_summary_part.done"
1319
+ | "response.reasoning_summary_text.delta"
1320
+ | "response.function_call_arguments.delta"
1321
+ | "response.function_call_arguments.done"
1322
+ | "response.code_interpreter_call_code.delta"
1323
+ | "response.code_interpreter_call_code.done"
1324
+ | "response.apply_patch_call_operation_diff.delta"
1325
+ | "response.apply_patch_call_operation_diff.done"
1326
+ | "response.image_generation_call.partial_image"
1327
+ | "error"
1328
+
1329
+ type KnownResponseStreamEvent = Extract<ResponseStreamEvent, { readonly type: KnownResponseStreamEventType }>
1330
+
1331
+ const knownResponseStreamEventTypes = new Set<KnownResponseStreamEventType>([
1332
+ "response.created",
1333
+ "response.completed",
1334
+ "response.incomplete",
1335
+ "response.failed",
1336
+ "response.output_item.added",
1337
+ "response.output_item.done",
1338
+ "response.output_text.delta",
1339
+ "response.output_text.annotation.added",
1340
+ "response.reasoning_summary_part.added",
1341
+ "response.reasoning_summary_part.done",
1342
+ "response.reasoning_summary_text.delta",
1343
+ "response.function_call_arguments.delta",
1344
+ "response.function_call_arguments.done",
1345
+ "response.code_interpreter_call_code.delta",
1346
+ "response.code_interpreter_call_code.done",
1347
+ "response.apply_patch_call_operation_diff.delta",
1348
+ "response.apply_patch_call_operation_diff.done",
1349
+ "response.image_generation_call.partial_image",
1350
+ "error"
1351
+ ])
1352
+
1353
+ const isKnownResponseStreamEvent = (
1354
+ event: ResponseStreamEvent
1355
+ ): event is KnownResponseStreamEvent => knownResponseStreamEventTypes.has(event.type as KnownResponseStreamEventType)
940
1356
 
941
1357
  const makeResponse = Effect.fnUntraced(
942
1358
  function*<Tools extends ReadonlyArray<Tool.Any>>({
@@ -946,7 +1362,7 @@ const makeResponse = Effect.fnUntraced(
946
1362
  toolNameMapper
947
1363
  }: {
948
1364
  readonly options: LanguageModel.ProviderOptions
949
- readonly rawResponse: Generated.Response
1365
+ readonly rawResponse: OpenAiSchema.Response
950
1366
  readonly response: HttpClientResponse.HttpClientResponse
951
1367
  readonly toolNameMapper: Tool.NameMapper<Tools>
952
1368
  }): Effect.fn.Return<
@@ -985,7 +1401,7 @@ const makeResponse = Effect.fnUntraced(
985
1401
  id: part.call_id,
986
1402
  name: toolName,
987
1403
  params: { call_id: part.call_id, operation: part.operation },
988
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1404
+ metadata: { openai: makeItemIdMetadata(part.id) }
989
1405
  })
990
1406
  break
991
1407
  }
@@ -1036,10 +1452,11 @@ const makeResponse = Effect.fnUntraced(
1036
1452
 
1037
1453
  case "function_call": {
1038
1454
  hasToolCalls = true
1455
+
1039
1456
  const toolName = part.name
1040
- const toolParams = part.arguments
1041
- const params = yield* Effect.try({
1042
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1457
+
1458
+ const toolParams = yield* Effect.try({
1459
+ try: () => Tool.unsafeSecureJsonParse(part.arguments),
1043
1460
  catch: (cause) =>
1044
1461
  AiError.make({
1045
1462
  module: "OpenAiLanguageModel",
@@ -1051,12 +1468,15 @@ const makeResponse = Effect.fnUntraced(
1051
1468
  })
1052
1469
  })
1053
1470
  })
1471
+
1472
+ const params = yield* transformToolCallParams(options.tools, part.name, toolParams)
1473
+
1054
1474
  parts.push({
1055
1475
  type: "tool-call",
1056
1476
  id: part.call_id,
1057
1477
  name: toolName,
1058
1478
  params,
1059
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1479
+ metadata: { openai: makeItemIdMetadata(part.id) }
1060
1480
  })
1061
1481
  break
1062
1482
  }
@@ -1087,7 +1507,7 @@ const makeResponse = Effect.fnUntraced(
1087
1507
  id: part.call_id,
1088
1508
  name: toolName,
1089
1509
  params: { action: part.action },
1090
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1510
+ metadata: { openai: makeItemIdMetadata(part.id) }
1091
1511
  })
1092
1512
  break
1093
1513
  }
@@ -1097,13 +1517,17 @@ const makeResponse = Effect.fnUntraced(
1097
1517
  ? (approvalRequests.get(part.approval_request_id) ?? part.id)
1098
1518
  : part.id
1099
1519
 
1100
- const toolName = `mcp.${part.name}`
1520
+ const { toolName, params } = yield* normalizeMcpToolCall({
1521
+ toolNameMapper,
1522
+ toolParams: part.arguments,
1523
+ method: "makeResponse"
1524
+ })
1101
1525
 
1102
1526
  parts.push({
1103
1527
  type: "tool-call",
1104
1528
  id: toolId,
1105
1529
  name: toolName,
1106
- params: part.arguments,
1530
+ params,
1107
1531
  providerExecuted: true
1108
1532
  })
1109
1533
 
@@ -1114,14 +1538,14 @@ const makeResponse = Effect.fnUntraced(
1114
1538
  isFailure: false,
1115
1539
  providerExecuted: true,
1116
1540
  result: {
1117
- type: "call",
1541
+ type: "mcp_call",
1118
1542
  name: part.name,
1119
1543
  arguments: part.arguments,
1120
1544
  server_label: part.server_label,
1121
1545
  ...(Predicate.isNotNullish(part.output) ? { output: part.output } : undefined),
1122
1546
  ...(Predicate.isNotNullish(part.error) ? { error: part.error } : undefined)
1123
1547
  },
1124
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1548
+ metadata: { openai: makeItemIdMetadata(part.id) }
1125
1549
  })
1126
1550
 
1127
1551
  break
@@ -1135,20 +1559,11 @@ const makeResponse = Effect.fnUntraced(
1135
1559
  case "mcp_approval_request": {
1136
1560
  const approvalRequestId = (part as any).approval_request_id ?? part.id
1137
1561
  const toolId = yield* idGenerator.generateId()
1138
- const toolName = `mcp.${part.name}`
1139
1562
 
1140
- const params = yield* Effect.try({
1141
- try: () => Tool.unsafeSecureJsonParse(part.arguments),
1142
- catch: (cause) =>
1143
- AiError.make({
1144
- module: "OpenAiLanguageModel",
1145
- method: "makeResponse",
1146
- reason: new AiError.ToolParameterValidationError({
1147
- toolName,
1148
- toolParams: {},
1149
- description: `Failed securely JSON parse tool parameters: ${cause}`
1150
- })
1151
- })
1563
+ const { toolName, params } = yield* normalizeMcpToolCall({
1564
+ toolNameMapper,
1565
+ toolParams: part.arguments,
1566
+ method: "makeResponse"
1152
1567
  })
1153
1568
 
1154
1569
  parts.push({
@@ -1296,7 +1711,7 @@ const makeResponse = Effect.fnUntraced(
1296
1711
  id: part.call_id,
1297
1712
  name: toolName,
1298
1713
  params: { action: part.action },
1299
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1714
+ metadata: { openai: makeItemIdMetadata(part.id) }
1300
1715
  })
1301
1716
  break
1302
1717
  }
@@ -1335,7 +1750,7 @@ const makeResponse = Effect.fnUntraced(
1335
1750
  reason: finishReason,
1336
1751
  usage: getUsage(rawResponse.usage),
1337
1752
  response: buildHttpResponseDetails(response),
1338
- ...(rawResponse.service_tier && { metadata: { openai: { serviceTier: rawResponse.service_tier } } })
1753
+ ...toServiceTier(rawResponse.service_tier)
1339
1754
  })
1340
1755
 
1341
1756
  return parts
@@ -1368,18 +1783,44 @@ const makeStreamResponse = Effect.fnUntraced(
1368
1783
  let hasToolCalls = false
1369
1784
 
1370
1785
  // Track annotations for current message to include in text-end metadata
1371
- const activeAnnotations: Array<typeof Generated.Annotation.Encoded> = []
1786
+ const activeAnnotations: Array<typeof OpenAiSchema.Annotation.Encoded> = []
1787
+
1788
+ type ReasoningSummaryPartStatus = "active" | "can-conclude" | "concluded"
1789
+ type ReasoningPart = {
1790
+ encryptedContent: string | undefined
1791
+ summaryParts: Record<number, ReasoningSummaryPartStatus>
1792
+ }
1372
1793
 
1373
1794
  // Track active reasoning items with state machine for proper concluding logic
1374
- const activeReasoning: Record<string, {
1375
- readonly encryptedContent: string | undefined
1376
- readonly summaryParts: Record<number, "active" | "can-conclude" | "concluded">
1377
- }> = {}
1795
+ const activeReasoning: Record<string, ReasoningPart> = {}
1796
+
1797
+ const getOrCreateReasoningPart = (
1798
+ itemId: string,
1799
+ encryptedContent?: string | null
1800
+ ): ReasoningPart => {
1801
+ const activePart = activeReasoning[itemId]
1802
+ if (Predicate.isNotUndefined(activePart)) {
1803
+ if (Predicate.isNotNullish(encryptedContent)) {
1804
+ activePart.encryptedContent = encryptedContent
1805
+ }
1806
+ return activePart
1807
+ }
1808
+
1809
+ const reasoningPart: ReasoningPart = {
1810
+ encryptedContent: Predicate.isNotNullish(encryptedContent) ? encryptedContent : undefined,
1811
+ summaryParts: {}
1812
+ }
1813
+ activeReasoning[itemId] = reasoningPart
1814
+ return reasoningPart
1815
+ }
1378
1816
 
1379
1817
  // Track active tool calls with optional provider-specific state
1380
1818
  const activeToolCalls: Record<number, {
1381
1819
  readonly id: string
1382
1820
  readonly name: string
1821
+ readonly functionCall?: {
1822
+ emitted: boolean
1823
+ }
1383
1824
  readonly applyPatch?: {
1384
1825
  hasDiff: boolean
1385
1826
  endEmitted: boolean
@@ -1399,6 +1840,10 @@ const makeStreamResponse = Effect.fnUntraced(
1399
1840
  Stream.mapEffect(Effect.fnUntraced(function*(event) {
1400
1841
  const parts: Array<Response.StreamPartEncoded> = []
1401
1842
 
1843
+ if (!isKnownResponseStreamEvent(event)) {
1844
+ return parts
1845
+ }
1846
+
1402
1847
  switch (event.type) {
1403
1848
  case "response.created": {
1404
1849
  const createdAt = new Date(event.response.created_at * 1000)
@@ -1428,7 +1873,7 @@ const makeStreamResponse = Effect.fnUntraced(
1428
1873
  ),
1429
1874
  usage: getUsage(event.response.usage),
1430
1875
  response: buildHttpResponseDetails(response),
1431
- ...(event.response.service_tier && { metadata: { openai: { serviceTier: event.response.service_tier } } })
1876
+ ...toServiceTier(event.response.service_tier)
1432
1877
  })
1433
1878
  break
1434
1879
  }
@@ -1529,7 +1974,8 @@ const makeStreamResponse = Effect.fnUntraced(
1529
1974
  case "function_call": {
1530
1975
  activeToolCalls[event.output_index] = {
1531
1976
  id: event.item.call_id,
1532
- name: event.item.name
1977
+ name: event.item.name,
1978
+ functionCall: { emitted: false }
1533
1979
  }
1534
1980
  parts.push({
1535
1981
  type: "tool-params-start",
@@ -1566,34 +2012,33 @@ const makeStreamResponse = Effect.fnUntraced(
1566
2012
  parts.push({
1567
2013
  type: "text-start",
1568
2014
  id: event.item.id,
1569
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2015
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1570
2016
  })
1571
2017
  break
1572
2018
  }
1573
2019
 
1574
2020
  case "reasoning": {
1575
- const encryptedContent = event.item.encrypted_content ?? undefined
1576
- activeReasoning[event.item.id] = {
1577
- encryptedContent,
1578
- summaryParts: { 0: "active" }
1579
- }
1580
- parts.push({
1581
- type: "reasoning-start",
1582
- id: `${event.item.id}:0`,
1583
- metadata: {
1584
- openai: {
1585
- ...makeItemIdMetadata(event.item.id),
1586
- ...makeEncryptedContentMetadata(event.item.encrypted_content)
2021
+ const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content)
2022
+ if (Predicate.isUndefined(reasoningPart.summaryParts[0])) {
2023
+ reasoningPart.summaryParts[0] = "active"
2024
+ parts.push({
2025
+ type: "reasoning-start",
2026
+ id: `${event.item.id}:0`,
2027
+ metadata: {
2028
+ openai: {
2029
+ ...makeItemIdMetadata(event.item.id),
2030
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
2031
+ }
1587
2032
  }
1588
- }
1589
- })
2033
+ })
2034
+ }
1590
2035
  break
1591
2036
  }
1592
2037
 
1593
2038
  case "shell_call": {
1594
2039
  const toolName = toolNameMapper.getCustomName("shell")
1595
2040
  activeToolCalls[event.output_index] = {
1596
- id: event.item.id,
2041
+ id: event.item.id ?? event.item.call_id,
1597
2042
  name: toolName
1598
2043
  }
1599
2044
  break
@@ -1644,7 +2089,7 @@ const makeStreamResponse = Effect.fnUntraced(
1644
2089
  parts.push({
1645
2090
  type: "tool-params-delta",
1646
2091
  id: toolCall.id,
1647
- delta: InternalUtilities.escapeJSONDelta(event.item.operation.diff)
2092
+ delta: InternalUtilities.escapeJSONDelta(event.item.operation.diff ?? "")
1648
2093
  })
1649
2094
  }
1650
2095
  parts.push({
@@ -1666,7 +2111,7 @@ const makeStreamResponse = Effect.fnUntraced(
1666
2111
  id: toolCall.id,
1667
2112
  name: toolName,
1668
2113
  params: { call_id: event.item.call_id, operation: event.item.operation },
1669
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2114
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1670
2115
  })
1671
2116
  }
1672
2117
  delete activeToolCalls[event.output_index]
@@ -1729,12 +2174,20 @@ const makeStreamResponse = Effect.fnUntraced(
1729
2174
  }
1730
2175
 
1731
2176
  case "function_call": {
2177
+ const toolCall = activeToolCalls[event.output_index]
2178
+ if (Predicate.isNotUndefined(toolCall?.functionCall?.emitted) && toolCall.functionCall.emitted) {
2179
+ delete activeToolCalls[event.output_index]
2180
+ break
2181
+ }
1732
2182
  delete activeToolCalls[event.output_index]
2183
+
1733
2184
  hasToolCalls = true
2185
+
1734
2186
  const toolName = event.item.name
1735
- const toolParams = event.item.arguments
1736
- const params = yield* Effect.try({
1737
- try: () => Tool.unsafeSecureJsonParse(toolParams),
2187
+ const toolArgs = event.item.arguments
2188
+
2189
+ const toolParams = yield* Effect.try({
2190
+ try: () => Tool.unsafeSecureJsonParse(toolArgs),
1738
2191
  catch: (cause) =>
1739
2192
  AiError.make({
1740
2193
  module: "OpenAiLanguageModel",
@@ -1746,17 +2199,22 @@ const makeStreamResponse = Effect.fnUntraced(
1746
2199
  })
1747
2200
  })
1748
2201
  })
2202
+
2203
+ const params = yield* transformToolCallParams(options.tools, toolName, toolParams)
2204
+
1749
2205
  parts.push({
1750
2206
  type: "tool-params-end",
1751
2207
  id: event.item.call_id
1752
2208
  })
2209
+
1753
2210
  parts.push({
1754
2211
  type: "tool-call",
1755
2212
  id: event.item.call_id,
1756
2213
  name: toolName,
1757
2214
  params,
1758
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2215
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1759
2216
  })
2217
+
1760
2218
  break
1761
2219
  }
1762
2220
 
@@ -1780,7 +2238,7 @@ const makeStreamResponse = Effect.fnUntraced(
1780
2238
  id: event.item.call_id,
1781
2239
  name: toolName,
1782
2240
  params: { action: event.item.action },
1783
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2241
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1784
2242
  })
1785
2243
  break
1786
2244
  }
@@ -1794,13 +2252,17 @@ const makeStreamResponse = Effect.fnUntraced(
1794
2252
  event.item.id)
1795
2253
  : event.item.id
1796
2254
 
1797
- const toolName = `mcp.${event.item.name}`
2255
+ const { toolName, params } = yield* normalizeMcpToolCall({
2256
+ toolNameMapper,
2257
+ toolParams: event.item.arguments,
2258
+ method: "makeStreamResponse"
2259
+ })
1798
2260
 
1799
2261
  parts.push({
1800
2262
  type: "tool-call",
1801
2263
  id: toolId,
1802
2264
  name: toolName,
1803
- params: event.item.arguments,
2265
+ params,
1804
2266
  providerExecuted: true
1805
2267
  })
1806
2268
 
@@ -1811,14 +2273,14 @@ const makeStreamResponse = Effect.fnUntraced(
1811
2273
  isFailure: false,
1812
2274
  providerExecuted: true,
1813
2275
  result: {
1814
- type: "call",
2276
+ type: "mcp_call",
1815
2277
  name: event.item.name,
1816
2278
  arguments: event.item.arguments,
1817
2279
  server_label: event.item.server_label,
1818
2280
  ...(Predicate.isNotNullish(event.item.output) ? { output: event.item.output } : undefined),
1819
2281
  ...(Predicate.isNotNullish(event.item.error) ? { error: event.item.error } : undefined)
1820
2282
  },
1821
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2283
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1822
2284
  })
1823
2285
 
1824
2286
  break
@@ -1833,12 +2295,16 @@ const makeStreamResponse = Effect.fnUntraced(
1833
2295
  const toolId = yield* idGenerator.generateId()
1834
2296
  const approvalRequestId = (event.item as any).approval_request_id ?? event.item.id
1835
2297
  streamApprovalRequests.set(approvalRequestId, toolId)
1836
- const toolName = `mcp.${event.item.name}`
2298
+ const { toolName, params } = yield* normalizeMcpToolCall({
2299
+ toolNameMapper,
2300
+ toolParams: event.item.arguments,
2301
+ method: "makeStreamResponse"
2302
+ })
1837
2303
  parts.push({
1838
2304
  type: "tool-call",
1839
2305
  id: toolId,
1840
2306
  name: toolName,
1841
- params: event.item.arguments,
2307
+ params,
1842
2308
  providerExecuted: true
1843
2309
  })
1844
2310
  parts.push({
@@ -1862,7 +2328,7 @@ const makeStreamResponse = Effect.fnUntraced(
1862
2328
  }
1863
2329
 
1864
2330
  case "reasoning": {
1865
- const reasoningPart = activeReasoning[event.item.id]
2331
+ const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content)
1866
2332
  for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
1867
2333
  if (status === "active" || status === "can-conclude") {
1868
2334
  parts.push({
@@ -1871,7 +2337,7 @@ const makeStreamResponse = Effect.fnUntraced(
1871
2337
  metadata: {
1872
2338
  openai: {
1873
2339
  ...makeItemIdMetadata(event.item.id),
1874
- ...makeEncryptedContentMetadata(event.item.encrypted_content)
2340
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
1875
2341
  }
1876
2342
  }
1877
2343
  })
@@ -1886,10 +2352,10 @@ const makeStreamResponse = Effect.fnUntraced(
1886
2352
  const toolName = toolNameMapper.getCustomName("shell")
1887
2353
  parts.push({
1888
2354
  type: "tool-call",
1889
- id: event.item.id,
2355
+ id: event.item.id ?? event.item.call_id,
1890
2356
  name: toolName,
1891
2357
  params: { action: event.item.action },
1892
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2358
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1893
2359
  })
1894
2360
  break
1895
2361
  }
@@ -1924,7 +2390,7 @@ const makeStreamResponse = Effect.fnUntraced(
1924
2390
  }
1925
2391
 
1926
2392
  case "response.output_text.annotation.added": {
1927
- const annotation = event.annotation as typeof Generated.Annotation.Encoded
2393
+ const annotation = event.annotation as typeof OpenAiSchema.Annotation.Encoded
1928
2394
  // Track annotation for text-end metadata
1929
2395
  activeAnnotations.push(annotation)
1930
2396
  if (annotation.type === "container_file_citation") {
@@ -2006,6 +2472,48 @@ const makeStreamResponse = Effect.fnUntraced(
2006
2472
  break
2007
2473
  }
2008
2474
 
2475
+ case "response.function_call_arguments.done": {
2476
+ const toolCall = activeToolCalls[event.output_index]
2477
+ if (
2478
+ Predicate.isNotUndefined(toolCall?.functionCall) &&
2479
+ !toolCall.functionCall.emitted
2480
+ ) {
2481
+ hasToolCalls = true
2482
+
2483
+ const toolParams = yield* Effect.try({
2484
+ try: () => Tool.unsafeSecureJsonParse(event.arguments),
2485
+ catch: (cause) =>
2486
+ AiError.make({
2487
+ module: "OpenAiLanguageModel",
2488
+ method: "makeStreamResponse",
2489
+ reason: new AiError.ToolParameterValidationError({
2490
+ toolName: toolCall.name,
2491
+ toolParams: {},
2492
+ description: `Failed securely JSON parse tool parameters: ${cause}`
2493
+ })
2494
+ })
2495
+ })
2496
+
2497
+ const params = yield* transformToolCallParams(options.tools, toolCall.name, toolParams)
2498
+
2499
+ parts.push({
2500
+ type: "tool-params-end",
2501
+ id: toolCall.id
2502
+ })
2503
+
2504
+ parts.push({
2505
+ type: "tool-call",
2506
+ id: toolCall.id,
2507
+ name: toolCall.name,
2508
+ params,
2509
+ metadata: { openai: makeItemIdMetadata(event.item_id) }
2510
+ })
2511
+
2512
+ toolCall.functionCall.emitted = true
2513
+ }
2514
+ break
2515
+ }
2516
+
2009
2517
  case "response.apply_patch_call_operation_diff.delta": {
2010
2518
  const toolCall = activeToolCalls[event.output_index]
2011
2519
  if (Predicate.isNotUndefined(toolCall?.applyPatch)) {
@@ -2095,28 +2603,28 @@ const makeStreamResponse = Effect.fnUntraced(
2095
2603
  }
2096
2604
 
2097
2605
  case "response.reasoning_summary_part.added": {
2098
- // The first reasoning start is pushed in the `response.output_item.added` block
2606
+ const reasoningPart = getOrCreateReasoningPart(event.item_id)
2099
2607
  if (event.summary_index > 0) {
2100
- const reasoningPart = activeReasoning[event.item_id]
2101
- if (Predicate.isNotUndefined(reasoningPart)) {
2102
- // Conclude all can-conclude parts before starting new one
2103
- for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
2104
- if (status === "can-conclude") {
2105
- parts.push({
2106
- type: "reasoning-end",
2107
- id: `${event.item_id}:${summaryIndex}`,
2108
- metadata: {
2109
- openai: {
2110
- ...makeItemIdMetadata(event.item_id),
2111
- ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
2112
- }
2608
+ // Conclude all can-conclude parts before starting new one
2609
+ for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
2610
+ if (status === "can-conclude") {
2611
+ parts.push({
2612
+ type: "reasoning-end",
2613
+ id: `${event.item_id}:${summaryIndex}`,
2614
+ metadata: {
2615
+ openai: {
2616
+ ...makeItemIdMetadata(event.item_id),
2617
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
2113
2618
  }
2114
- })
2115
- reasoningPart.summaryParts[Number(summaryIndex)] = "concluded"
2116
- }
2619
+ }
2620
+ })
2621
+ reasoningPart.summaryParts[Number(summaryIndex)] = "concluded"
2117
2622
  }
2118
- reasoningPart.summaryParts[event.summary_index] = "active"
2119
2623
  }
2624
+ }
2625
+
2626
+ if (Predicate.isUndefined(reasoningPart.summaryParts[event.summary_index])) {
2627
+ reasoningPart.summaryParts[event.summary_index] = "active"
2120
2628
  parts.push({
2121
2629
  type: "reasoning-start",
2122
2630
  id: `${event.item_id}:${event.summary_index}`,
@@ -2136,26 +2644,27 @@ const makeStreamResponse = Effect.fnUntraced(
2136
2644
  type: "reasoning-delta",
2137
2645
  id: `${event.item_id}:${event.summary_index}`,
2138
2646
  delta: event.delta,
2139
- metadata: { openai: { ...makeItemIdMetadata(event.item_id) } }
2647
+ metadata: { openai: makeItemIdMetadata(event.item_id) }
2140
2648
  })
2141
2649
  break
2142
2650
  }
2143
2651
 
2144
2652
  case "response.reasoning_summary_part.done": {
2653
+ const reasoningPart = getOrCreateReasoningPart(event.item_id)
2145
2654
  // When OpenAI stores message data, we can immediately conclude the
2146
2655
  // reasoning part given that we do not need the encrypted content
2147
2656
  if (config.store === true) {
2148
2657
  parts.push({
2149
2658
  type: "reasoning-end",
2150
2659
  id: `${event.item_id}:${event.summary_index}`,
2151
- metadata: { openai: { ...makeItemIdMetadata(event.item_id) } }
2660
+ metadata: { openai: makeItemIdMetadata(event.item_id) }
2152
2661
  })
2153
2662
  // Mark the summary part concluded
2154
- activeReasoning[event.item_id].summaryParts[event.summary_index] = "concluded"
2663
+ reasoningPart.summaryParts[event.summary_index] = "concluded"
2155
2664
  } else {
2156
2665
  // Mark the summary part as can-conclude given we still need a
2157
2666
  // final summary part with the encrypted content
2158
- activeReasoning[event.item_id].summaryParts[event.summary_index] = "can-conclude"
2667
+ reasoningPart.summaryParts[event.summary_index] = "can-conclude"
2159
2668
  }
2160
2669
  break
2161
2670
  }
@@ -2174,7 +2683,7 @@ const makeStreamResponse = Effect.fnUntraced(
2174
2683
 
2175
2684
  const annotateRequest = (
2176
2685
  span: Span,
2177
- request: typeof Generated.CreateResponse.Encoded
2686
+ request: typeof OpenAiSchema.CreateResponse.Encoded
2178
2687
  ): void => {
2179
2688
  addGenAIAnnotations(span, {
2180
2689
  system: "openai",
@@ -2194,7 +2703,7 @@ const annotateRequest = (
2194
2703
  })
2195
2704
  }
2196
2705
 
2197
- const annotateResponse = (span: Span, response: Generated.Response): void => {
2706
+ const annotateResponse = (span: Span, response: OpenAiSchema.Response): void => {
2198
2707
  const finishReason = response.incomplete_details?.reason as string | undefined
2199
2708
  addGenAIAnnotations(span, {
2200
2709
  response: {
@@ -2244,7 +2753,7 @@ const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) =>
2244
2753
  // Tool Conversion
2245
2754
  // =============================================================================
2246
2755
 
2247
- type OpenAiToolChoice = typeof Generated.CreateResponse.Encoded["tool_choice"]
2756
+ type OpenAiToolChoice = typeof OpenAiSchema.CreateResponse.Encoded["tool_choice"]
2248
2757
 
2249
2758
  const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>({
2250
2759
  config,
@@ -2255,7 +2764,7 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2255
2764
  readonly options: LanguageModel.ProviderOptions
2256
2765
  readonly toolNameMapper: Tool.NameMapper<Tools>
2257
2766
  }): Effect.fn.Return<{
2258
- readonly tools: ReadonlyArray<typeof Generated.Tool.Encoded> | undefined
2767
+ readonly tools: ReadonlyArray<typeof OpenAiSchema.Tool.Encoded> | undefined
2259
2768
  readonly toolChoice: OpenAiToolChoice | undefined
2260
2769
  }, AiError.AiError> {
2261
2770
  // Return immediately if no tools are in the toolkit
@@ -2263,7 +2772,7 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2263
2772
  return { tools: undefined, toolChoice: undefined }
2264
2773
  }
2265
2774
 
2266
- const tools: Array<typeof Generated.Tool.Encoded> = []
2775
+ const tools: Array<typeof OpenAiSchema.Tool.Encoded> = []
2267
2776
  let toolChoice: OpenAiToolChoice | undefined = undefined
2268
2777
 
2269
2778
  // Filter the incoming tools down to the set of allowed tools as indicated by
@@ -2279,14 +2788,16 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2279
2788
 
2280
2789
  // Convert the tools in the toolkit to the provider-defined format
2281
2790
  for (const tool of allowedTools) {
2282
- if (Tool.isUserDefined(tool)) {
2791
+ if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) {
2283
2792
  const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true
2793
+ const description = Tool.getDescription(tool)
2794
+ const parameters = yield* tryToolJsonSchema(tool, "prepareTools")
2284
2795
  tools.push({
2285
2796
  type: "function",
2286
2797
  name: tool.name,
2287
- description: Tool.getDescription(tool) ?? null,
2288
- parameters: Tool.getJsonSchema(tool) as { readonly [x: string]: Schema.Json },
2289
- strict
2798
+ parameters,
2799
+ strict,
2800
+ ...(Predicate.isNotUndefined(description) ? { description } : undefined)
2290
2801
  })
2291
2802
  }
2292
2803
 
@@ -2468,35 +2979,63 @@ const getStatus = (
2468
2979
  | Prompt.TextPart
2469
2980
  | Prompt.ToolCallPart
2470
2981
  | Prompt.ToolResultPart
2471
- ): typeof Generated.Message.Encoded["status"] | null => part.options.openai?.status ?? null
2982
+ ): typeof OpenAiSchema.MessageStatus.Encoded | null => part.options.openai?.status ?? null
2472
2983
  const getEncryptedContent = (
2473
2984
  part: Prompt.ReasoningPart
2474
2985
  ): string | null => part.options.openai?.encryptedContent ?? null
2475
2986
 
2476
2987
  const getImageDetail = (part: Prompt.FilePart): ImageDetail => part.options.openai?.imageDetail ?? "auto"
2477
2988
 
2478
- const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefined(itemId) ? { itemId } : undefined
2989
+ const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefined(itemId) ? { itemId } : {}
2479
2990
 
2480
2991
  const makeEncryptedContentMetadata = (encryptedContent: string | null | undefined) =>
2481
2992
  Predicate.isNotNullish(encryptedContent) ? { encryptedContent } : undefined
2482
2993
 
2483
- const prepareResponseFormat = ({ config, options }: {
2994
+ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError =>
2995
+ AiError.make({
2996
+ module: "OpenAiLanguageModel",
2997
+ method,
2998
+ reason: new AiError.UnsupportedSchemaError({
2999
+ description: error instanceof Error ? error.message : String(error)
3000
+ })
3001
+ })
3002
+
3003
+ const tryCodecTransform = <S extends Schema.Top>(schema: S, method: string) =>
3004
+ Effect.try({
3005
+ try: () => toCodecOpenAI(schema),
3006
+ catch: (error) => unsupportedSchemaError(error, method)
3007
+ })
3008
+
3009
+ const tryJsonSchema = <S extends Schema.Top>(schema: S, method: string) =>
3010
+ Effect.try({
3011
+ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }),
3012
+ catch: (error) => unsupportedSchemaError(error, method)
3013
+ })
3014
+
3015
+ const tryToolJsonSchema = <T extends Tool.Any>(tool: T, method: string) =>
3016
+ Effect.try({
3017
+ try: () => Tool.getJsonSchema(tool, { transformer: toCodecOpenAI }),
3018
+ catch: (error) => unsupportedSchemaError(error, method)
3019
+ })
3020
+
3021
+ const prepareResponseFormat = Effect.fnUntraced(function*({ config, options }: {
2484
3022
  readonly config: typeof Config.Service
2485
3023
  readonly options: LanguageModel.ProviderOptions
2486
- }): typeof Generated.TextResponseFormatConfiguration.Encoded => {
3024
+ }): Effect.fn.Return<typeof OpenAiSchema.TextResponseFormatConfiguration.Encoded, AiError.AiError> {
2487
3025
  if (options.responseFormat.type === "json") {
2488
3026
  const name = options.responseFormat.objectName
2489
3027
  const schema = options.responseFormat.schema
3028
+ const jsonSchema = yield* tryJsonSchema(schema, "prepareResponseFormat")
2490
3029
  return {
2491
3030
  type: "json_schema",
2492
3031
  name,
2493
3032
  description: AST.resolveDescription(schema.ast) ?? "Response with a JSON object",
2494
- schema: Tool.getJsonSchemaFromSchema(schema) as any,
3033
+ schema: jsonSchema,
2495
3034
  strict: config.strictJsonSchema ?? true
2496
3035
  }
2497
3036
  }
2498
3037
  return { type: "text" }
2499
- }
3038
+ })
2500
3039
 
2501
3040
  interface ModelCapabilities {
2502
3041
  readonly isReasoningModel: boolean
@@ -2570,7 +3109,42 @@ const getApprovalRequestIdMapping = (prompt: Prompt.Prompt): ReadonlyMap<string,
2570
3109
  return mapping
2571
3110
  }
2572
3111
 
2573
- const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.Usage => {
3112
+ const normalizeMcpToolCall = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>({
3113
+ toolNameMapper,
3114
+ toolParams,
3115
+ method
3116
+ }: {
3117
+ readonly toolNameMapper: Tool.NameMapper<Tools>
3118
+ readonly toolParams: unknown
3119
+ readonly method: string
3120
+ }): Effect.fn.Return<{
3121
+ readonly toolName: string
3122
+ readonly params: unknown
3123
+ }, AiError.AiError> {
3124
+ const toolName = toolNameMapper.getCustomName("mcp")
3125
+
3126
+ if (typeof toolParams !== "string") {
3127
+ return { toolName, params: toolParams }
3128
+ }
3129
+
3130
+ const params = yield* Effect.try({
3131
+ try: () => Tool.unsafeSecureJsonParse(toolParams),
3132
+ catch: (cause) =>
3133
+ AiError.make({
3134
+ module: "OpenAiLanguageModel",
3135
+ method,
3136
+ reason: new AiError.ToolParameterValidationError({
3137
+ toolName,
3138
+ toolParams,
3139
+ description: `Failed to securely JSON parse tool parameters: ${cause}`
3140
+ })
3141
+ })
3142
+ })
3143
+
3144
+ return { toolName, params }
3145
+ })
3146
+
3147
+ const getUsage = (usage: OpenAiSchema.ResponseUsage | null | undefined): Response.Usage => {
2574
3148
  if (Predicate.isNullish(usage)) {
2575
3149
  return {
2576
3150
  inputTokens: {
@@ -2589,8 +3163,8 @@ const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.U
2589
3163
 
2590
3164
  const inputTokens = usage.input_tokens
2591
3165
  const outputTokens = usage.output_tokens
2592
- const cachedTokens = usage.input_tokens_details.cached_tokens
2593
- const reasoningTokens = usage.output_tokens_details.reasoning_tokens
3166
+ const cachedTokens = getUsageTokenDetail(usage.input_tokens_details, "cached_tokens")
3167
+ const reasoningTokens = getUsageTokenDetail(usage.output_tokens_details, "reasoning_tokens")
2594
3168
 
2595
3169
  return {
2596
3170
  inputTokens: {
@@ -2606,3 +3180,64 @@ const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.U
2606
3180
  }
2607
3181
  }
2608
3182
  }
3183
+
3184
+ type ServiceTier = "default" | "auto" | "flex" | "scale" | "priority" | null
3185
+
3186
+ const toServiceTier = (value: string | undefined): {
3187
+ readonly metadata: {
3188
+ readonly openai: {
3189
+ readonly serviceTier: ServiceTier
3190
+ }
3191
+ }
3192
+ } | undefined => {
3193
+ switch (value) {
3194
+ case "default":
3195
+ case "auto":
3196
+ case "flex":
3197
+ case "scale":
3198
+ case "priority":
3199
+ return { metadata: { openai: { serviceTier: value } } }
3200
+ default:
3201
+ return undefined
3202
+ }
3203
+ }
3204
+
3205
+ const getUsageTokenDetail = (details: unknown, key: string): number =>
3206
+ Predicate.hasProperty(details, key) && typeof details[key] === "number" ? details[key] : 0
3207
+
3208
+ const transformToolCallParams = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>(
3209
+ tools: Tools,
3210
+ toolName: string,
3211
+ toolParams: unknown
3212
+ ): Effect.fn.Return<unknown, AiError.AiError> {
3213
+ const tool = tools.find((tool) => tool.name === toolName)
3214
+
3215
+ if (Predicate.isUndefined(tool)) {
3216
+ return yield* AiError.make({
3217
+ module: "OpenAiLanguageModel",
3218
+ method: "makeResponse",
3219
+ reason: new AiError.ToolNotFoundError({
3220
+ toolName,
3221
+ availableTools: tools.map((tool) => tool.name)
3222
+ })
3223
+ })
3224
+ }
3225
+
3226
+ const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse")
3227
+
3228
+ const transform = Schema.decodeEffect(codec)
3229
+
3230
+ return yield* (
3231
+ transform(toolParams) as Effect.Effect<unknown, Schema.SchemaError>
3232
+ ).pipe(Effect.mapError((error) =>
3233
+ AiError.make({
3234
+ module: "OpenAiLanguageModel",
3235
+ method: "makeResponse",
3236
+ reason: new AiError.ToolParameterValidationError({
3237
+ toolName,
3238
+ toolParams,
3239
+ description: error.issue.toString()
3240
+ })
3241
+ })
3242
+ ))
3243
+ })