@effect/ai-openai 4.0.0-beta.8 → 4.0.0-beta.80

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 +66675 -37672
  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 +170 -29
  6. package/dist/OpenAiClient.d.ts.map +1 -1
  7. package/dist/OpenAiClient.js +321 -46
  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 +88 -10
  14. package/dist/OpenAiConfig.d.ts.map +1 -1
  15. package/dist/OpenAiConfig.js +42 -7
  16. package/dist/OpenAiConfig.js.map +1 -1
  17. package/dist/OpenAiEmbeddingModel.d.ts +188 -0
  18. package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
  19. package/dist/OpenAiEmbeddingModel.js +194 -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 +357 -63
  25. package/dist/OpenAiLanguageModel.d.ts.map +1 -1
  26. package/dist/OpenAiLanguageModel.js +390 -168
  27. package/dist/OpenAiLanguageModel.js.map +1 -1
  28. package/dist/OpenAiSchema.d.ts +2325 -0
  29. package/dist/OpenAiSchema.d.ts.map +1 -0
  30. package/dist/OpenAiSchema.js +811 -0
  31. package/dist/OpenAiSchema.js.map +1 -0
  32. package/dist/OpenAiTelemetry.d.ts +63 -22
  33. package/dist/OpenAiTelemetry.d.ts.map +1 -1
  34. package/dist/OpenAiTelemetry.js +20 -10
  35. package/dist/OpenAiTelemetry.js.map +1 -1
  36. package/dist/OpenAiTool.d.ts +148 -62
  37. package/dist/OpenAiTool.d.ts.map +1 -1
  38. package/dist/OpenAiTool.js +125 -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 +9717 -4887
  48. package/src/OpenAiClient.ts +499 -98
  49. package/src/OpenAiClientGenerated.ts +202 -0
  50. package/src/OpenAiConfig.ts +89 -11
  51. package/src/OpenAiEmbeddingModel.ts +332 -0
  52. package/src/OpenAiError.ts +170 -35
  53. package/src/OpenAiLanguageModel.ts +776 -169
  54. package/src/OpenAiSchema.ts +1286 -0
  55. package/src/OpenAiTelemetry.ts +69 -28
  56. package/src/OpenAiTool.ts +126 -40
  57. package/src/index.ts +22 -33
  58. package/src/internal/errors.ts +6 -4
@@ -1,28 +1,32 @@
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
+ * non-streaming or streaming response results back into Effect AI response
7
+ * content and metadata.
3
8
  *
4
- * Provides a LanguageModel implementation for OpenAI's responses API,
5
- * supporting text generation, structured output, tool calling, and streaming.
6
- *
7
- * @since 1.0.0
9
+ * @since 4.0.0
8
10
  */
11
+ import * as Context from "effect/Context"
9
12
  import * as DateTime from "effect/DateTime"
10
13
  import * as Effect from "effect/Effect"
11
14
  import * as Encoding from "effect/Encoding"
12
15
  import { dual } from "effect/Function"
13
16
  import * as Layer from "effect/Layer"
17
+ import * as Option from "effect/Option"
14
18
  import * as Predicate from "effect/Predicate"
15
19
  import * as Redactable from "effect/Redactable"
16
20
  import * as Schema from "effect/Schema"
17
21
  import * as AST from "effect/SchemaAST"
18
- import * as ServiceMap from "effect/ServiceMap"
19
22
  import * as Stream from "effect/Stream"
20
23
  import type { Span } from "effect/Tracer"
21
- import type { DeepMutable, Simplify } from "effect/Types"
24
+ import type { DeepMutable, Mutable, Simplify } from "effect/Types"
22
25
  import * as AiError from "effect/unstable/ai/AiError"
23
26
  import * as IdGenerator from "effect/unstable/ai/IdGenerator"
24
27
  import * as LanguageModel from "effect/unstable/ai/LanguageModel"
25
28
  import * as AiModel from "effect/unstable/ai/Model"
29
+ import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput"
26
30
  import type * as Prompt from "effect/unstable/ai/Prompt"
27
31
  import type * as Response from "effect/unstable/ai/Response"
28
32
  import * as Tool from "effect/unstable/ai/Tool"
@@ -31,6 +35,7 @@ import type * as HttpClientResponse from "effect/unstable/http/HttpClientRespons
31
35
  import * as Generated from "./Generated.ts"
32
36
  import * as InternalUtilities from "./internal/utilities.ts"
33
37
  import { OpenAiClient } from "./OpenAiClient.ts"
38
+ import type * as OpenAiSchema from "./OpenAiSchema.ts"
34
39
  import { addGenAIAnnotations } from "./OpenAiTelemetry.ts"
35
40
  import type * as OpenAiTool from "./OpenAiTool.ts"
36
41
 
@@ -38,8 +43,10 @@ const ResponseModelIds = Generated.ModelIdsResponses.members[1]
38
43
  const SharedModelIds = Generated.ModelIdsShared.members[1]
39
44
 
40
45
  /**
41
- * @since 1.0.0
46
+ * OpenAI model identifiers supported by the Responses API language model.
47
+ *
42
48
  * @category models
49
+ * @since 4.0.0
43
50
  */
44
51
  export type Model = typeof ResponseModelIds.Encoded | typeof SharedModelIds.Encoded
45
52
 
@@ -53,17 +60,29 @@ type ImageDetail = "auto" | "low" | "high"
53
60
  // =============================================================================
54
61
 
55
62
  /**
56
- * Service definition for OpenAI language model configuration.
63
+ * Context service for OpenAI language model configuration.
64
+ *
65
+ * **When to use**
66
+ *
67
+ * Use when you need to provide OpenAI Responses API request defaults through
68
+ * Effect context for language model operations.
69
+ *
70
+ * **Details**
71
+ *
72
+ * Config values are merged with the config object passed to `model`, `make`, or
73
+ * `layer`, with scoped context values taking precedence.
74
+ *
75
+ * @see {@link withConfigOverride} for scoping language model request overrides
57
76
  *
58
- * @since 1.0.0
59
77
  * @category services
78
+ * @since 4.0.0
60
79
  */
61
- export class Config extends ServiceMap.Service<
80
+ export class Config extends Context.Service<
62
81
  Config,
63
82
  Simplify<
64
83
  & Partial<
65
84
  Omit<
66
- typeof Generated.CreateResponse.Encoded,
85
+ typeof OpenAiSchema.CreateResponse.Encoded,
67
86
  "input" | "tools" | "tool_choice" | "stream" | "text"
68
87
  >
69
88
  >
@@ -105,7 +124,16 @@ export class Config extends ServiceMap.Service<
105
124
  // =============================================================================
106
125
 
107
126
  declare module "effect/unstable/ai/Prompt" {
127
+ /**
128
+ * OpenAI-specific options for file prompt parts.
129
+ *
130
+ * @category request
131
+ * @since 4.0.0
132
+ */
108
133
  export interface FilePartOptions extends ProviderOptions {
134
+ /**
135
+ * Provider-specific file options for the OpenAI Responses API.
136
+ */
109
137
  readonly openai?: {
110
138
  /**
111
139
  * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`.
@@ -114,7 +142,16 @@ declare module "effect/unstable/ai/Prompt" {
114
142
  } | null
115
143
  }
116
144
 
145
+ /**
146
+ * OpenAI-specific options for reasoning prompt parts.
147
+ *
148
+ * @category request
149
+ * @since 4.0.0
150
+ */
117
151
  export interface ReasoningPartOptions extends ProviderOptions {
152
+ /**
153
+ * Provider-specific reasoning options for the OpenAI Responses API.
154
+ */
118
155
  readonly openai?: {
119
156
  /**
120
157
  * The ID of the item to reference.
@@ -129,7 +166,16 @@ declare module "effect/unstable/ai/Prompt" {
129
166
  } | null
130
167
  }
131
168
 
169
+ /**
170
+ * OpenAI-specific options for assistant tool-call prompt parts.
171
+ *
172
+ * @category request
173
+ * @since 4.0.0
174
+ */
132
175
  export interface ToolCallPartOptions extends ProviderOptions {
176
+ /**
177
+ * Provider-specific tool-call options for the OpenAI Responses API.
178
+ */
133
179
  readonly openai?: {
134
180
  /**
135
181
  * The ID of the item to reference.
@@ -138,7 +184,7 @@ declare module "effect/unstable/ai/Prompt" {
138
184
  /**
139
185
  * The status of item.
140
186
  */
141
- readonly status?: typeof Generated.Message.Encoded["status"] | null
187
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
142
188
  /**
143
189
  * The ID of the approval request.
144
190
  */
@@ -146,7 +192,16 @@ declare module "effect/unstable/ai/Prompt" {
146
192
  } | null
147
193
  }
148
194
 
195
+ /**
196
+ * OpenAI-specific options for tool-result prompt parts.
197
+ *
198
+ * @category request
199
+ * @since 4.0.0
200
+ */
149
201
  export interface ToolResultPartOptions extends ProviderOptions {
202
+ /**
203
+ * Provider-specific tool-result options for the OpenAI Responses API.
204
+ */
150
205
  readonly openai?: {
151
206
  /**
152
207
  * The ID of the item to reference.
@@ -155,7 +210,7 @@ declare module "effect/unstable/ai/Prompt" {
155
210
  /**
156
211
  * The status of item.
157
212
  */
158
- readonly status?: typeof Generated.Message.Encoded["status"] | null
213
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
159
214
  /**
160
215
  * The ID of the approval request.
161
216
  */
@@ -163,7 +218,16 @@ declare module "effect/unstable/ai/Prompt" {
163
218
  } | null
164
219
  }
165
220
 
221
+ /**
222
+ * OpenAI-specific options for text prompt parts.
223
+ *
224
+ * @category request
225
+ * @since 4.0.0
226
+ */
166
227
  export interface TextPartOptions extends ProviderOptions {
228
+ /**
229
+ * Provider-specific text options for the OpenAI Responses API.
230
+ */
167
231
  readonly openai?: {
168
232
  /**
169
233
  * The ID of the item to reference.
@@ -172,18 +236,30 @@ declare module "effect/unstable/ai/Prompt" {
172
236
  /**
173
237
  * The status of item.
174
238
  */
175
- readonly status?: typeof Generated.Message.Encoded["status"] | null
239
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
176
240
  /**
177
241
  * A list of annotations that apply to the output text.
178
242
  */
179
- readonly annotations?: ReadonlyArray<typeof Generated.Annotation.Encoded> | null
243
+ readonly annotations?: ReadonlyArray<typeof OpenAiSchema.Annotation.Encoded> | null
180
244
  } | null
181
245
  }
182
246
  }
183
247
 
184
248
  declare module "effect/unstable/ai/Response" {
249
+ /**
250
+ * OpenAI metadata attached to a complete text response part.
251
+ *
252
+ * @category response
253
+ * @since 4.0.0
254
+ */
185
255
  export interface TextPartMetadata extends ProviderMetadata {
256
+ /**
257
+ * Provider-specific metadata returned for the text part.
258
+ */
186
259
  readonly openai?: {
260
+ /**
261
+ * The OpenAI item ID associated with the text part.
262
+ */
187
263
  readonly itemId?: string | null
188
264
  /**
189
265
  * If the model emits a refusal content part, the refusal explanation
@@ -194,63 +270,171 @@ declare module "effect/unstable/ai/Response" {
194
270
  /**
195
271
  * The status of item.
196
272
  */
197
- readonly status?: typeof Generated.Message.Encoded["status"] | null
273
+ readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null
198
274
  /**
199
275
  * The text content part annotations.
200
276
  */
201
- readonly annotations?: ReadonlyArray<typeof Generated.Annotation.Encoded> | null
277
+ readonly annotations?: ReadonlyArray<typeof OpenAiSchema.Annotation.Encoded> | null
202
278
  }
203
279
  }
204
280
 
281
+ /**
282
+ * OpenAI metadata emitted when a streamed text part starts.
283
+ *
284
+ * @category response
285
+ * @since 4.0.0
286
+ */
205
287
  export interface TextStartPartMetadata extends ProviderMetadata {
288
+ /**
289
+ * Provider-specific metadata returned for the streamed text start.
290
+ */
206
291
  readonly openai?: {
292
+ /**
293
+ * The OpenAI item ID associated with the streamed text part.
294
+ */
207
295
  readonly itemId?: string | null
208
296
  } | null
209
297
  }
210
298
 
299
+ /**
300
+ * OpenAI metadata emitted when a streamed text part ends.
301
+ *
302
+ * @category response
303
+ * @since 4.0.0
304
+ */
211
305
  export interface TextEndPartMetadata extends ProviderMetadata {
306
+ /**
307
+ * Provider-specific metadata returned for the streamed text end.
308
+ */
212
309
  readonly openai?: {
310
+ /**
311
+ * The OpenAI item ID associated with the streamed text part.
312
+ */
213
313
  readonly itemId?: string | null
214
- readonly annotations?: ReadonlyArray<typeof Generated.Annotation.Encoded> | null
314
+ /**
315
+ * The annotations collected for the completed streamed text part.
316
+ */
317
+ readonly annotations?: ReadonlyArray<typeof OpenAiSchema.Annotation.Encoded> | null
215
318
  } | null
216
319
  }
217
320
 
321
+ /**
322
+ * OpenAI metadata attached to a complete reasoning response part.
323
+ *
324
+ * @category response
325
+ * @since 4.0.0
326
+ */
218
327
  export interface ReasoningPartMetadata extends ProviderMetadata {
328
+ /**
329
+ * Provider-specific metadata returned for the reasoning part.
330
+ */
219
331
  readonly openai?: {
332
+ /**
333
+ * The OpenAI item ID associated with the reasoning part.
334
+ */
220
335
  readonly itemId?: string | null
336
+ /**
337
+ * Encrypted reasoning content that can be sent back in later requests.
338
+ */
221
339
  readonly encryptedContent?: string | null
222
340
  } | null
223
341
  }
224
342
 
343
+ /**
344
+ * OpenAI metadata emitted when a streamed reasoning part starts.
345
+ *
346
+ * @category response
347
+ * @since 4.0.0
348
+ */
225
349
  export interface ReasoningStartPartMetadata extends ProviderMetadata {
350
+ /**
351
+ * Provider-specific metadata returned for the streamed reasoning start.
352
+ */
226
353
  readonly openai?: {
354
+ /**
355
+ * The OpenAI item ID associated with the reasoning part.
356
+ */
227
357
  readonly itemId?: string | null
358
+ /**
359
+ * Encrypted reasoning content that can be sent back in later requests.
360
+ */
228
361
  readonly encryptedContent?: string | null
229
362
  } | null
230
363
  }
231
364
 
365
+ /**
366
+ * OpenAI metadata emitted for a streamed reasoning delta.
367
+ *
368
+ * @category response
369
+ * @since 4.0.0
370
+ */
232
371
  export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
372
+ /**
373
+ * Provider-specific metadata returned for the streamed reasoning delta.
374
+ */
233
375
  readonly openai?: {
376
+ /**
377
+ * The OpenAI item ID associated with the reasoning part.
378
+ */
234
379
  readonly itemId?: string | null
235
380
  } | null
236
381
  }
237
382
 
383
+ /**
384
+ * OpenAI metadata emitted when a streamed reasoning part ends.
385
+ *
386
+ * @category response
387
+ * @since 4.0.0
388
+ */
238
389
  export interface ReasoningEndPartMetadata extends ProviderMetadata {
390
+ /**
391
+ * Provider-specific metadata returned for the streamed reasoning end.
392
+ */
239
393
  readonly openai?: {
394
+ /**
395
+ * The OpenAI item ID associated with the reasoning part.
396
+ */
240
397
  readonly itemId?: string | null
398
+ /**
399
+ * Encrypted reasoning content that can be sent back in later requests.
400
+ */
241
401
  readonly encryptedContent?: string
242
402
  } | null
243
403
  }
244
404
 
405
+ /**
406
+ * OpenAI metadata attached to tool-call response parts.
407
+ *
408
+ * @category response
409
+ * @since 4.0.0
410
+ */
245
411
  export interface ToolCallPartMetadata extends ProviderMetadata {
412
+ /**
413
+ * Provider-specific metadata returned for the tool call.
414
+ */
246
415
  readonly openai?: {
416
+ /**
417
+ * The OpenAI item ID associated with the tool call.
418
+ */
247
419
  readonly itemId?: string | null
248
420
  } | null
249
421
  }
250
422
 
423
+ /**
424
+ * OpenAI metadata attached to document source citations.
425
+ *
426
+ * @category response
427
+ * @since 4.0.0
428
+ */
251
429
  export interface DocumentSourcePartMetadata extends ProviderMetadata {
430
+ /**
431
+ * Provider-specific citation metadata for the OpenAI Responses API.
432
+ */
252
433
  readonly openai?:
253
434
  | {
435
+ /**
436
+ * Identifies a citation to an uploaded file.
437
+ */
254
438
  readonly type: "file_citation"
255
439
  /**
256
440
  * The index of the file in the list of files.
@@ -262,6 +446,9 @@ declare module "effect/unstable/ai/Response" {
262
446
  readonly fileId: string
263
447
  }
264
448
  | {
449
+ /**
450
+ * Identifies a citation to a generated file path.
451
+ */
265
452
  readonly type: "file_path"
266
453
  /**
267
454
  * The index of the file in the list of files.
@@ -273,6 +460,9 @@ declare module "effect/unstable/ai/Response" {
273
460
  readonly fileId: string
274
461
  }
275
462
  | {
463
+ /**
464
+ * Identifies a citation to a file inside a container.
465
+ */
276
466
  readonly type: "container_file_citation"
277
467
  /**
278
468
  * The ID of the file.
@@ -286,8 +476,20 @@ declare module "effect/unstable/ai/Response" {
286
476
  | null
287
477
  }
288
478
 
479
+ /**
480
+ * OpenAI metadata attached to URL source citations.
481
+ *
482
+ * @category response
483
+ * @since 4.0.0
484
+ */
289
485
  export interface UrlSourcePartMetadata extends ProviderMetadata {
486
+ /**
487
+ * Provider-specific URL citation metadata for the OpenAI Responses API.
488
+ */
290
489
  readonly openai?: {
490
+ /**
491
+ * Identifies a citation to a URL.
492
+ */
291
493
  readonly type: "url_citation"
292
494
  /**
293
495
  * The index of the first character of the URL citation in the message.
@@ -300,8 +502,20 @@ declare module "effect/unstable/ai/Response" {
300
502
  } | null
301
503
  }
302
504
 
505
+ /**
506
+ * OpenAI metadata attached to finish response parts.
507
+ *
508
+ * @category response
509
+ * @since 4.0.0
510
+ */
303
511
  export interface FinishPartMetadata extends ProviderMetadata {
512
+ /**
513
+ * Provider-specific metadata returned when generation finishes.
514
+ */
304
515
  readonly openai?: {
516
+ /**
517
+ * The service tier reported by OpenAI for the response.
518
+ */
305
519
  readonly serviceTier?: "default" | "auto" | "flex" | "scale" | "priority" | null
306
520
  } | null
307
521
  }
@@ -312,31 +526,58 @@ declare module "effect/unstable/ai/Response" {
312
526
  // =============================================================================
313
527
 
314
528
  /**
315
- * @since 1.0.0
529
+ * Creates an OpenAI model descriptor that can be provided with
530
+ * `Effect.provide`.
531
+ *
532
+ * **When to use**
533
+ *
534
+ * Use when you want an OpenAI language model value that carries provider and
535
+ * model metadata and can be supplied directly to an Effect program.
536
+ *
537
+ * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly
538
+ * @see {@link make} for constructing the language model service effectfully
539
+ *
316
540
  * @category constructors
541
+ * @since 4.0.0
317
542
  */
318
543
  export const model = (
319
544
  model: (string & {}) | Model,
320
545
  config?: Omit<typeof Config.Service, "model">
321
546
  ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> =>
322
- AiModel.make("openai", layer({ model, config }))
547
+ AiModel.make("openai", model, layer({ model, config }))
323
548
 
324
549
  // TODO
325
550
  // /**
326
- // * @since 1.0.0
551
+ // * @since 4.0.0
327
552
  // * @category constructors
328
553
  // */
329
554
  // export const modelWithTokenizer = (
330
555
  // model: (string & {}) | Model,
331
556
  // config?: Omit<typeof Config.Service, "model">
332
557
  // ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
333
- // AiModel.make("openai", layerWithTokenizer({ model, config }))
558
+ // AiModel.make("openai", model, layerWithTokenizer({ model, config }))
334
559
 
335
560
  /**
336
- * Creates an OpenAI language model service.
561
+ * Creates an OpenAI `LanguageModel` service from a model identifier and
562
+ * optional request defaults.
563
+ *
564
+ * **When to use**
565
+ *
566
+ * Use to construct an OpenAI Responses API language model service backed by
567
+ * `OpenAiClient`.
568
+ *
569
+ * **Details**
570
+ *
571
+ * The returned effect requires `OpenAiClient`. Request defaults from the
572
+ * `config` option are merged with any `Config` service in the context, with
573
+ * context values taking precedence. The service supports both `generateText`
574
+ * and `streamText`.
575
+ *
576
+ * @see {@link layer} for providing the service as a `Layer`
577
+ * @see {@link model} for creating a model descriptor for `Effect.provide`
337
578
  *
338
- * @since 1.0.0
339
579
  * @category constructors
580
+ * @since 4.0.0
340
581
  */
341
582
  export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
342
583
  readonly model: (string & {}) | Model
@@ -345,7 +586,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
345
586
  const client = yield* OpenAiClient
346
587
 
347
588
  const makeConfig = Effect.gen(function*() {
348
- const services = yield* Effect.services<never>()
589
+ const services = yield* Effect.context<never>()
349
590
  return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
350
591
  })
351
592
 
@@ -354,9 +595,9 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
354
595
  readonly config: typeof Config.Service
355
596
  readonly options: LanguageModel.ProviderOptions
356
597
  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!)
598
+ }): Effect.fn.Return<typeof OpenAiSchema.CreateResponse.Encoded, AiError.AiError> {
599
+ const include = new Set<typeof OpenAiSchema.IncludeEnum.Encoded>()
600
+ const capabilities = getModelCapabilities(config.model as string)
360
601
  const messages = yield* prepareMessages({
361
602
  config,
362
603
  options,
@@ -369,26 +610,29 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
369
610
  options,
370
611
  toolNameMapper
371
612
  })
372
- const responseFormat = prepareResponseFormat({
613
+ const responseFormat = yield* prepareResponseFormat({
373
614
  config,
374
615
  options
375
616
  })
376
- const request: typeof Generated.CreateResponse.Encoded = {
377
- ...config,
617
+ const { fileIdPrefixes: _fip, strictJsonSchema: _sjs, ...apiConfig } = config
618
+ const request: Mutable<typeof OpenAiSchema.CreateResponse.Encoded> = {
619
+ ...apiConfig,
378
620
  input: messages,
379
- include: include.size > 0 ? Array.from(include) : null,
621
+ include: include.size > 0 ? Array.from(include) : undefined,
380
622
  text: {
381
- verbosity: config.text?.verbosity ?? null,
623
+ verbosity: config.text?.verbosity ?? undefined,
382
624
  format: responseFormat
383
- },
384
- ...(Predicate.isNotUndefined(tools) ? { tools } : undefined),
385
- ...(Predicate.isNotUndefined(toolChoice) ? { tool_choice: toolChoice } : undefined)
625
+ }
386
626
  }
627
+ if (tools) request.tools = tools
628
+ if (toolChoice) request.tool_choice = toolChoice
629
+ if (options.previousResponseId) request.previous_response_id = options.previousResponseId
387
630
  return request
388
631
  }
389
632
  )
390
633
 
391
634
  return yield* LanguageModel.make({
635
+ codecTransformer: toCodecOpenAI,
392
636
  generateText: Effect.fnUntraced(
393
637
  function*(options) {
394
638
  const config = yield* makeConfig
@@ -433,10 +677,27 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
433
677
  })
434
678
 
435
679
  /**
436
- * Creates a layer for the OpenAI language model.
680
+ * Creates a layer that provides the OpenAI `LanguageModel.LanguageModel`
681
+ * service.
682
+ *
683
+ * **When to use**
684
+ *
685
+ * Use when composing application layers and you want OpenAI to satisfy
686
+ * `LanguageModel.LanguageModel` while supplying `OpenAiClient` from another
687
+ * layer.
688
+ *
689
+ * **Details**
690
+ *
691
+ * The `config` option supplies request defaults for the selected model. Scoped
692
+ * values from `withConfigOverride` are merged when each request is built and
693
+ * take precedence over these defaults.
694
+ *
695
+ * @see {@link make} for constructing the language model service effectfully
696
+ * @see {@link model} for creating a model descriptor for `Effect.provide`
697
+ * @see {@link withConfigOverride} for scoped request configuration overrides
437
698
  *
438
- * @since 1.0.0
439
699
  * @category layers
700
+ * @since 4.0.0
440
701
  */
441
702
  export const layer = (options: {
442
703
  readonly model: (string & {}) | Model
@@ -445,39 +706,114 @@ export const layer = (options: {
445
706
  Layer.effect(LanguageModel.LanguageModel, make(options))
446
707
 
447
708
  /**
448
- * Provides config overrides for OpenAI language model operations.
709
+ * Provides scoped config overrides for OpenAI language model operations.
710
+ *
711
+ * **When to use**
712
+ *
713
+ * Use to apply OpenAI Responses API config overrides around one or more
714
+ * language model operations without changing the defaults passed to `model`,
715
+ * `make`, or `layer`.
716
+ *
717
+ * **Details**
718
+ *
719
+ * The override is dual, so it can be used in pipe form or as
720
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
721
+ * existing `Config` service in the current context, and the override values take
722
+ * precedence.
723
+ *
724
+ * @see {@link Config} for the scoped configuration service consumed by this function
449
725
  *
450
- * @since 1.0.0
451
726
  * @category configuration
727
+ * @since 4.0.0
452
728
  */
453
729
  export const withConfigOverride: {
454
730
  /**
455
- * Provides config overrides for OpenAI language model operations.
731
+ * Provides scoped config overrides for OpenAI language model operations.
732
+ *
733
+ * **When to use**
734
+ *
735
+ * Use to apply OpenAI Responses API config overrides around one or more
736
+ * language model operations without changing the defaults passed to `model`,
737
+ * `make`, or `layer`.
738
+ *
739
+ * **Details**
740
+ *
741
+ * The override is dual, so it can be used in pipe form or as
742
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
743
+ * existing `Config` service in the current context, and the override values take
744
+ * precedence.
745
+ *
746
+ * @see {@link Config} for the scoped configuration service consumed by this function
456
747
  *
457
- * @since 1.0.0
458
748
  * @category configuration
749
+ * @since 4.0.0
459
750
  */
460
751
  (overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
461
752
  /**
462
- * Provides config overrides for OpenAI language model operations.
753
+ * Provides scoped config overrides for OpenAI language model operations.
754
+ *
755
+ * **When to use**
756
+ *
757
+ * Use to apply OpenAI Responses API config overrides around one or more
758
+ * language model operations without changing the defaults passed to `model`,
759
+ * `make`, or `layer`.
760
+ *
761
+ * **Details**
762
+ *
763
+ * The override is dual, so it can be used in pipe form or as
764
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
765
+ * existing `Config` service in the current context, and the override values take
766
+ * precedence.
767
+ *
768
+ * @see {@link Config} for the scoped configuration service consumed by this function
463
769
  *
464
- * @since 1.0.0
465
770
  * @category configuration
771
+ * @since 4.0.0
466
772
  */
467
773
  <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
468
774
  } = dual<
469
775
  /**
470
- * Provides config overrides for OpenAI language model operations.
776
+ * Provides scoped config overrides for OpenAI language model operations.
777
+ *
778
+ * **When to use**
779
+ *
780
+ * Use to apply OpenAI Responses API config overrides around one or more
781
+ * language model operations without changing the defaults passed to `model`,
782
+ * `make`, or `layer`.
783
+ *
784
+ * **Details**
785
+ *
786
+ * The override is dual, so it can be used in pipe form or as
787
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
788
+ * existing `Config` service in the current context, and the override values take
789
+ * precedence.
790
+ *
791
+ * @see {@link Config} for the scoped configuration service consumed by this function
471
792
  *
472
- * @since 1.0.0
473
793
  * @category configuration
794
+ * @since 4.0.0
474
795
  */
475
796
  (overrides: typeof Config.Service) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
476
797
  /**
477
- * Provides config overrides for OpenAI language model operations.
798
+ * Provides scoped config overrides for OpenAI language model operations.
799
+ *
800
+ * **When to use**
801
+ *
802
+ * Use to apply OpenAI Responses API config overrides around one or more
803
+ * language model operations without changing the defaults passed to `model`,
804
+ * `make`, or `layer`.
805
+ *
806
+ * **Details**
807
+ *
808
+ * The override is dual, so it can be used in pipe form or as
809
+ * `withConfigOverride(effect, overrides)`. Overrides are merged with any
810
+ * existing `Config` service in the current context, and the override values take
811
+ * precedence.
812
+ *
813
+ * @see {@link Config} for the scoped configuration service consumed by this function
478
814
  *
479
- * @since 1.0.0
480
815
  * @category configuration
816
+ * @since 4.0.0
481
817
  */
482
818
  <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
483
819
  >(2, (self, overrides) =>
@@ -512,10 +848,10 @@ const prepareMessages = Effect.fnUntraced(
512
848
  }: {
513
849
  readonly config: typeof Config.Service
514
850
  readonly options: LanguageModel.ProviderOptions
515
- readonly include: Set<typeof Generated.IncludeEnum.Encoded>
851
+ readonly include: Set<typeof OpenAiSchema.IncludeEnum.Encoded>
516
852
  readonly capabilities: ModelCapabilities
517
853
  readonly toolNameMapper: Tool.NameMapper<Tools>
518
- }): Effect.fn.Return<ReadonlyArray<typeof Generated.InputItem.Encoded>, AiError.AiError> {
854
+ }): Effect.fn.Return<ReadonlyArray<typeof OpenAiSchema.InputItem.Encoded>, AiError.AiError> {
519
855
  const processedApprovalIds = new Set<string>()
520
856
 
521
857
  const hasConversation = Predicate.isNotNullish(config.conversation)
@@ -547,27 +883,28 @@ const prepareMessages = Effect.fnUntraced(
547
883
  if (config.store === false && capabilities.isReasoningModel) {
548
884
  include.add("reasoning.encrypted_content")
549
885
  }
550
- if (Predicate.isNotUndefined(codeInterpreterTool)) {
886
+ if (codeInterpreterTool) {
551
887
  include.add("code_interpreter_call.outputs")
552
888
  }
553
- if (Predicate.isNotUndefined(webSearchTool) || Predicate.isNotUndefined(webSearchPreviewTool)) {
889
+ if (webSearchTool || webSearchPreviewTool) {
554
890
  include.add("web_search_call.action.sources")
555
891
  }
556
892
 
557
- const messages: Array<typeof Generated.InputItem.Encoded> = []
893
+ const messages: Array<typeof OpenAiSchema.InputItem.Encoded> = []
894
+ const prompt = options.incrementalPrompt ?? options.prompt
558
895
 
559
- for (const message of options.prompt.content) {
896
+ for (const message of prompt.content) {
560
897
  switch (message.role) {
561
898
  case "system": {
562
899
  messages.push({
563
- role: getSystemMessageMode(config.model!),
900
+ role: getSystemMessageMode(config.model as string),
564
901
  content: message.content
565
902
  })
566
903
  break
567
904
  }
568
905
 
569
906
  case "user": {
570
- const content: Array<typeof Generated.InputContent.Encoded> = []
907
+ const content: Array<typeof OpenAiSchema.InputContent.Encoded> = []
571
908
 
572
909
  for (let index = 0; index < message.content.length; index++) {
573
910
  const part = message.content[index]
@@ -630,7 +967,7 @@ const prepareMessages = Effect.fnUntraced(
630
967
  }
631
968
 
632
969
  case "assistant": {
633
- const reasoningMessages: Record<string, DeepMutable<typeof Generated.ReasoningItem.Encoded>> = {}
970
+ const reasoningMessages: Record<string, DeepMutable<typeof OpenAiSchema.ReasoningItem.Encoded>> = {}
634
971
 
635
972
  for (const part of message.content) {
636
973
  switch (part.type) {
@@ -689,7 +1026,7 @@ const prepareMessages = Effect.fnUntraced(
689
1026
  }
690
1027
  }
691
1028
  } else {
692
- const summaryParts: Array<typeof Generated.SummaryTextContent.Encoded> = []
1029
+ const summaryParts: Array<typeof OpenAiSchema.SummaryTextContent.Encoded> = []
693
1030
 
694
1031
  if (part.text.length > 0) {
695
1032
  summaryParts.push({ type: "summary_text", text: part.text })
@@ -700,7 +1037,9 @@ const prepareMessages = Effect.fnUntraced(
700
1037
  type: "reasoning",
701
1038
  id,
702
1039
  summary: summaryParts,
703
- encrypted_content: encryptedContent ?? null
1040
+ ...(Predicate.isNotNull(encryptedContent)
1041
+ ? { encrypted_content: encryptedContent }
1042
+ : undefined)
704
1043
  }
705
1044
 
706
1045
  messages.push(reasoningMessages[id])
@@ -921,7 +1260,7 @@ const buildHttpRequestDetails = (
921
1260
  method: request.method,
922
1261
  url: request.url,
923
1262
  urlParams: Array.from(request.urlParams),
924
- hash: request.hash,
1263
+ hash: Option.getOrUndefined(request.hash),
925
1264
  headers: Redactable.redact(request.headers) as Record<string, string>
926
1265
  })
927
1266
 
@@ -936,7 +1275,56 @@ const buildHttpResponseDetails = (
936
1275
  // Response Conversion
937
1276
  // =============================================================================
938
1277
 
939
- type ResponseStreamEvent = typeof Generated.ResponseStreamEvent.Type
1278
+ type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type
1279
+
1280
+ type KnownResponseStreamEventType =
1281
+ | "response.created"
1282
+ | "response.completed"
1283
+ | "response.incomplete"
1284
+ | "response.failed"
1285
+ | "response.output_item.added"
1286
+ | "response.output_item.done"
1287
+ | "response.output_text.delta"
1288
+ | "response.output_text.annotation.added"
1289
+ | "response.reasoning_summary_part.added"
1290
+ | "response.reasoning_summary_part.done"
1291
+ | "response.reasoning_summary_text.delta"
1292
+ | "response.function_call_arguments.delta"
1293
+ | "response.function_call_arguments.done"
1294
+ | "response.code_interpreter_call_code.delta"
1295
+ | "response.code_interpreter_call_code.done"
1296
+ | "response.apply_patch_call_operation_diff.delta"
1297
+ | "response.apply_patch_call_operation_diff.done"
1298
+ | "response.image_generation_call.partial_image"
1299
+ | "error"
1300
+
1301
+ type KnownResponseStreamEvent = Extract<ResponseStreamEvent, { readonly type: KnownResponseStreamEventType }>
1302
+
1303
+ const knownResponseStreamEventTypes = new Set<KnownResponseStreamEventType>([
1304
+ "response.created",
1305
+ "response.completed",
1306
+ "response.incomplete",
1307
+ "response.failed",
1308
+ "response.output_item.added",
1309
+ "response.output_item.done",
1310
+ "response.output_text.delta",
1311
+ "response.output_text.annotation.added",
1312
+ "response.reasoning_summary_part.added",
1313
+ "response.reasoning_summary_part.done",
1314
+ "response.reasoning_summary_text.delta",
1315
+ "response.function_call_arguments.delta",
1316
+ "response.function_call_arguments.done",
1317
+ "response.code_interpreter_call_code.delta",
1318
+ "response.code_interpreter_call_code.done",
1319
+ "response.apply_patch_call_operation_diff.delta",
1320
+ "response.apply_patch_call_operation_diff.done",
1321
+ "response.image_generation_call.partial_image",
1322
+ "error"
1323
+ ])
1324
+
1325
+ const isKnownResponseStreamEvent = (
1326
+ event: ResponseStreamEvent
1327
+ ): event is KnownResponseStreamEvent => knownResponseStreamEventTypes.has(event.type as KnownResponseStreamEventType)
940
1328
 
941
1329
  const makeResponse = Effect.fnUntraced(
942
1330
  function*<Tools extends ReadonlyArray<Tool.Any>>({
@@ -946,7 +1334,7 @@ const makeResponse = Effect.fnUntraced(
946
1334
  toolNameMapper
947
1335
  }: {
948
1336
  readonly options: LanguageModel.ProviderOptions
949
- readonly rawResponse: Generated.Response
1337
+ readonly rawResponse: OpenAiSchema.Response
950
1338
  readonly response: HttpClientResponse.HttpClientResponse
951
1339
  readonly toolNameMapper: Tool.NameMapper<Tools>
952
1340
  }): Effect.fn.Return<
@@ -985,7 +1373,7 @@ const makeResponse = Effect.fnUntraced(
985
1373
  id: part.call_id,
986
1374
  name: toolName,
987
1375
  params: { call_id: part.call_id, operation: part.operation },
988
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1376
+ metadata: { openai: makeItemIdMetadata(part.id) }
989
1377
  })
990
1378
  break
991
1379
  }
@@ -1036,10 +1424,11 @@ const makeResponse = Effect.fnUntraced(
1036
1424
 
1037
1425
  case "function_call": {
1038
1426
  hasToolCalls = true
1427
+
1039
1428
  const toolName = part.name
1040
- const toolParams = part.arguments
1041
- const params = yield* Effect.try({
1042
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1429
+
1430
+ const toolParams = yield* Effect.try({
1431
+ try: () => Tool.unsafeSecureJsonParse(part.arguments),
1043
1432
  catch: (cause) =>
1044
1433
  AiError.make({
1045
1434
  module: "OpenAiLanguageModel",
@@ -1051,12 +1440,15 @@ const makeResponse = Effect.fnUntraced(
1051
1440
  })
1052
1441
  })
1053
1442
  })
1443
+
1444
+ const params = yield* transformToolCallParams(options.tools, part.name, toolParams)
1445
+
1054
1446
  parts.push({
1055
1447
  type: "tool-call",
1056
1448
  id: part.call_id,
1057
1449
  name: toolName,
1058
1450
  params,
1059
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1451
+ metadata: { openai: makeItemIdMetadata(part.id) }
1060
1452
  })
1061
1453
  break
1062
1454
  }
@@ -1087,7 +1479,7 @@ const makeResponse = Effect.fnUntraced(
1087
1479
  id: part.call_id,
1088
1480
  name: toolName,
1089
1481
  params: { action: part.action },
1090
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1482
+ metadata: { openai: makeItemIdMetadata(part.id) }
1091
1483
  })
1092
1484
  break
1093
1485
  }
@@ -1097,13 +1489,17 @@ const makeResponse = Effect.fnUntraced(
1097
1489
  ? (approvalRequests.get(part.approval_request_id) ?? part.id)
1098
1490
  : part.id
1099
1491
 
1100
- const toolName = `mcp.${part.name}`
1492
+ const { toolName, params } = yield* normalizeMcpToolCall({
1493
+ toolNameMapper,
1494
+ toolParams: part.arguments,
1495
+ method: "makeResponse"
1496
+ })
1101
1497
 
1102
1498
  parts.push({
1103
1499
  type: "tool-call",
1104
1500
  id: toolId,
1105
1501
  name: toolName,
1106
- params: part.arguments,
1502
+ params,
1107
1503
  providerExecuted: true
1108
1504
  })
1109
1505
 
@@ -1114,14 +1510,14 @@ const makeResponse = Effect.fnUntraced(
1114
1510
  isFailure: false,
1115
1511
  providerExecuted: true,
1116
1512
  result: {
1117
- type: "call",
1513
+ type: "mcp_call",
1118
1514
  name: part.name,
1119
1515
  arguments: part.arguments,
1120
1516
  server_label: part.server_label,
1121
1517
  ...(Predicate.isNotNullish(part.output) ? { output: part.output } : undefined),
1122
1518
  ...(Predicate.isNotNullish(part.error) ? { error: part.error } : undefined)
1123
1519
  },
1124
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1520
+ metadata: { openai: makeItemIdMetadata(part.id) }
1125
1521
  })
1126
1522
 
1127
1523
  break
@@ -1135,20 +1531,11 @@ const makeResponse = Effect.fnUntraced(
1135
1531
  case "mcp_approval_request": {
1136
1532
  const approvalRequestId = (part as any).approval_request_id ?? part.id
1137
1533
  const toolId = yield* idGenerator.generateId()
1138
- const toolName = `mcp.${part.name}`
1139
1534
 
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
- })
1535
+ const { toolName, params } = yield* normalizeMcpToolCall({
1536
+ toolNameMapper,
1537
+ toolParams: part.arguments,
1538
+ method: "makeResponse"
1152
1539
  })
1153
1540
 
1154
1541
  parts.push({
@@ -1296,7 +1683,7 @@ const makeResponse = Effect.fnUntraced(
1296
1683
  id: part.call_id,
1297
1684
  name: toolName,
1298
1685
  params: { action: part.action },
1299
- metadata: { openai: { ...makeItemIdMetadata(part.id) } }
1686
+ metadata: { openai: makeItemIdMetadata(part.id) }
1300
1687
  })
1301
1688
  break
1302
1689
  }
@@ -1335,7 +1722,7 @@ const makeResponse = Effect.fnUntraced(
1335
1722
  reason: finishReason,
1336
1723
  usage: getUsage(rawResponse.usage),
1337
1724
  response: buildHttpResponseDetails(response),
1338
- ...(rawResponse.service_tier && { metadata: { openai: { serviceTier: rawResponse.service_tier } } })
1725
+ ...toServiceTier(rawResponse.service_tier)
1339
1726
  })
1340
1727
 
1341
1728
  return parts
@@ -1368,18 +1755,44 @@ const makeStreamResponse = Effect.fnUntraced(
1368
1755
  let hasToolCalls = false
1369
1756
 
1370
1757
  // Track annotations for current message to include in text-end metadata
1371
- const activeAnnotations: Array<typeof Generated.Annotation.Encoded> = []
1758
+ const activeAnnotations: Array<typeof OpenAiSchema.Annotation.Encoded> = []
1759
+
1760
+ type ReasoningSummaryPartStatus = "active" | "can-conclude" | "concluded"
1761
+ type ReasoningPart = {
1762
+ encryptedContent: string | undefined
1763
+ summaryParts: Record<number, ReasoningSummaryPartStatus>
1764
+ }
1372
1765
 
1373
1766
  // 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
- }> = {}
1767
+ const activeReasoning: Record<string, ReasoningPart> = {}
1768
+
1769
+ const getOrCreateReasoningPart = (
1770
+ itemId: string,
1771
+ encryptedContent?: string | null
1772
+ ): ReasoningPart => {
1773
+ const activePart = activeReasoning[itemId]
1774
+ if (Predicate.isNotUndefined(activePart)) {
1775
+ if (Predicate.isNotNullish(encryptedContent)) {
1776
+ activePart.encryptedContent = encryptedContent
1777
+ }
1778
+ return activePart
1779
+ }
1780
+
1781
+ const reasoningPart: ReasoningPart = {
1782
+ encryptedContent: Predicate.isNotNullish(encryptedContent) ? encryptedContent : undefined,
1783
+ summaryParts: {}
1784
+ }
1785
+ activeReasoning[itemId] = reasoningPart
1786
+ return reasoningPart
1787
+ }
1378
1788
 
1379
1789
  // Track active tool calls with optional provider-specific state
1380
1790
  const activeToolCalls: Record<number, {
1381
1791
  readonly id: string
1382
1792
  readonly name: string
1793
+ readonly functionCall?: {
1794
+ emitted: boolean
1795
+ }
1383
1796
  readonly applyPatch?: {
1384
1797
  hasDiff: boolean
1385
1798
  endEmitted: boolean
@@ -1399,6 +1812,10 @@ const makeStreamResponse = Effect.fnUntraced(
1399
1812
  Stream.mapEffect(Effect.fnUntraced(function*(event) {
1400
1813
  const parts: Array<Response.StreamPartEncoded> = []
1401
1814
 
1815
+ if (!isKnownResponseStreamEvent(event)) {
1816
+ return parts
1817
+ }
1818
+
1402
1819
  switch (event.type) {
1403
1820
  case "response.created": {
1404
1821
  const createdAt = new Date(event.response.created_at * 1000)
@@ -1428,7 +1845,7 @@ const makeStreamResponse = Effect.fnUntraced(
1428
1845
  ),
1429
1846
  usage: getUsage(event.response.usage),
1430
1847
  response: buildHttpResponseDetails(response),
1431
- ...(event.response.service_tier && { metadata: { openai: { serviceTier: event.response.service_tier } } })
1848
+ ...toServiceTier(event.response.service_tier)
1432
1849
  })
1433
1850
  break
1434
1851
  }
@@ -1529,7 +1946,8 @@ const makeStreamResponse = Effect.fnUntraced(
1529
1946
  case "function_call": {
1530
1947
  activeToolCalls[event.output_index] = {
1531
1948
  id: event.item.call_id,
1532
- name: event.item.name
1949
+ name: event.item.name,
1950
+ functionCall: { emitted: false }
1533
1951
  }
1534
1952
  parts.push({
1535
1953
  type: "tool-params-start",
@@ -1566,34 +1984,33 @@ const makeStreamResponse = Effect.fnUntraced(
1566
1984
  parts.push({
1567
1985
  type: "text-start",
1568
1986
  id: event.item.id,
1569
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
1987
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1570
1988
  })
1571
1989
  break
1572
1990
  }
1573
1991
 
1574
1992
  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)
1993
+ const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content)
1994
+ if (Predicate.isUndefined(reasoningPart.summaryParts[0])) {
1995
+ reasoningPart.summaryParts[0] = "active"
1996
+ parts.push({
1997
+ type: "reasoning-start",
1998
+ id: `${event.item.id}:0`,
1999
+ metadata: {
2000
+ openai: {
2001
+ ...makeItemIdMetadata(event.item.id),
2002
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
2003
+ }
1587
2004
  }
1588
- }
1589
- })
2005
+ })
2006
+ }
1590
2007
  break
1591
2008
  }
1592
2009
 
1593
2010
  case "shell_call": {
1594
2011
  const toolName = toolNameMapper.getCustomName("shell")
1595
2012
  activeToolCalls[event.output_index] = {
1596
- id: event.item.id,
2013
+ id: event.item.id ?? event.item.call_id,
1597
2014
  name: toolName
1598
2015
  }
1599
2016
  break
@@ -1644,7 +2061,7 @@ const makeStreamResponse = Effect.fnUntraced(
1644
2061
  parts.push({
1645
2062
  type: "tool-params-delta",
1646
2063
  id: toolCall.id,
1647
- delta: InternalUtilities.escapeJSONDelta(event.item.operation.diff)
2064
+ delta: InternalUtilities.escapeJSONDelta(event.item.operation.diff ?? "")
1648
2065
  })
1649
2066
  }
1650
2067
  parts.push({
@@ -1666,7 +2083,7 @@ const makeStreamResponse = Effect.fnUntraced(
1666
2083
  id: toolCall.id,
1667
2084
  name: toolName,
1668
2085
  params: { call_id: event.item.call_id, operation: event.item.operation },
1669
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2086
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1670
2087
  })
1671
2088
  }
1672
2089
  delete activeToolCalls[event.output_index]
@@ -1729,12 +2146,20 @@ const makeStreamResponse = Effect.fnUntraced(
1729
2146
  }
1730
2147
 
1731
2148
  case "function_call": {
2149
+ const toolCall = activeToolCalls[event.output_index]
2150
+ if (Predicate.isNotUndefined(toolCall?.functionCall?.emitted) && toolCall.functionCall.emitted) {
2151
+ delete activeToolCalls[event.output_index]
2152
+ break
2153
+ }
1732
2154
  delete activeToolCalls[event.output_index]
2155
+
1733
2156
  hasToolCalls = true
2157
+
1734
2158
  const toolName = event.item.name
1735
- const toolParams = event.item.arguments
1736
- const params = yield* Effect.try({
1737
- try: () => Tool.unsafeSecureJsonParse(toolParams),
2159
+ const toolArgs = event.item.arguments
2160
+
2161
+ const toolParams = yield* Effect.try({
2162
+ try: () => Tool.unsafeSecureJsonParse(toolArgs),
1738
2163
  catch: (cause) =>
1739
2164
  AiError.make({
1740
2165
  module: "OpenAiLanguageModel",
@@ -1746,17 +2171,22 @@ const makeStreamResponse = Effect.fnUntraced(
1746
2171
  })
1747
2172
  })
1748
2173
  })
2174
+
2175
+ const params = yield* transformToolCallParams(options.tools, toolName, toolParams)
2176
+
1749
2177
  parts.push({
1750
2178
  type: "tool-params-end",
1751
2179
  id: event.item.call_id
1752
2180
  })
2181
+
1753
2182
  parts.push({
1754
2183
  type: "tool-call",
1755
2184
  id: event.item.call_id,
1756
2185
  name: toolName,
1757
2186
  params,
1758
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2187
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1759
2188
  })
2189
+
1760
2190
  break
1761
2191
  }
1762
2192
 
@@ -1780,7 +2210,7 @@ const makeStreamResponse = Effect.fnUntraced(
1780
2210
  id: event.item.call_id,
1781
2211
  name: toolName,
1782
2212
  params: { action: event.item.action },
1783
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2213
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1784
2214
  })
1785
2215
  break
1786
2216
  }
@@ -1794,13 +2224,17 @@ const makeStreamResponse = Effect.fnUntraced(
1794
2224
  event.item.id)
1795
2225
  : event.item.id
1796
2226
 
1797
- const toolName = `mcp.${event.item.name}`
2227
+ const { toolName, params } = yield* normalizeMcpToolCall({
2228
+ toolNameMapper,
2229
+ toolParams: event.item.arguments,
2230
+ method: "makeStreamResponse"
2231
+ })
1798
2232
 
1799
2233
  parts.push({
1800
2234
  type: "tool-call",
1801
2235
  id: toolId,
1802
2236
  name: toolName,
1803
- params: event.item.arguments,
2237
+ params,
1804
2238
  providerExecuted: true
1805
2239
  })
1806
2240
 
@@ -1811,14 +2245,14 @@ const makeStreamResponse = Effect.fnUntraced(
1811
2245
  isFailure: false,
1812
2246
  providerExecuted: true,
1813
2247
  result: {
1814
- type: "call",
2248
+ type: "mcp_call",
1815
2249
  name: event.item.name,
1816
2250
  arguments: event.item.arguments,
1817
2251
  server_label: event.item.server_label,
1818
2252
  ...(Predicate.isNotNullish(event.item.output) ? { output: event.item.output } : undefined),
1819
2253
  ...(Predicate.isNotNullish(event.item.error) ? { error: event.item.error } : undefined)
1820
2254
  },
1821
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2255
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1822
2256
  })
1823
2257
 
1824
2258
  break
@@ -1833,12 +2267,16 @@ const makeStreamResponse = Effect.fnUntraced(
1833
2267
  const toolId = yield* idGenerator.generateId()
1834
2268
  const approvalRequestId = (event.item as any).approval_request_id ?? event.item.id
1835
2269
  streamApprovalRequests.set(approvalRequestId, toolId)
1836
- const toolName = `mcp.${event.item.name}`
2270
+ const { toolName, params } = yield* normalizeMcpToolCall({
2271
+ toolNameMapper,
2272
+ toolParams: event.item.arguments,
2273
+ method: "makeStreamResponse"
2274
+ })
1837
2275
  parts.push({
1838
2276
  type: "tool-call",
1839
2277
  id: toolId,
1840
2278
  name: toolName,
1841
- params: event.item.arguments,
2279
+ params,
1842
2280
  providerExecuted: true
1843
2281
  })
1844
2282
  parts.push({
@@ -1862,7 +2300,7 @@ const makeStreamResponse = Effect.fnUntraced(
1862
2300
  }
1863
2301
 
1864
2302
  case "reasoning": {
1865
- const reasoningPart = activeReasoning[event.item.id]
2303
+ const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content)
1866
2304
  for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
1867
2305
  if (status === "active" || status === "can-conclude") {
1868
2306
  parts.push({
@@ -1871,7 +2309,7 @@ const makeStreamResponse = Effect.fnUntraced(
1871
2309
  metadata: {
1872
2310
  openai: {
1873
2311
  ...makeItemIdMetadata(event.item.id),
1874
- ...makeEncryptedContentMetadata(event.item.encrypted_content)
2312
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
1875
2313
  }
1876
2314
  }
1877
2315
  })
@@ -1886,10 +2324,10 @@ const makeStreamResponse = Effect.fnUntraced(
1886
2324
  const toolName = toolNameMapper.getCustomName("shell")
1887
2325
  parts.push({
1888
2326
  type: "tool-call",
1889
- id: event.item.id,
2327
+ id: event.item.id ?? event.item.call_id,
1890
2328
  name: toolName,
1891
2329
  params: { action: event.item.action },
1892
- metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
2330
+ metadata: { openai: makeItemIdMetadata(event.item.id) }
1893
2331
  })
1894
2332
  break
1895
2333
  }
@@ -1924,7 +2362,7 @@ const makeStreamResponse = Effect.fnUntraced(
1924
2362
  }
1925
2363
 
1926
2364
  case "response.output_text.annotation.added": {
1927
- const annotation = event.annotation as typeof Generated.Annotation.Encoded
2365
+ const annotation = event.annotation as typeof OpenAiSchema.Annotation.Encoded
1928
2366
  // Track annotation for text-end metadata
1929
2367
  activeAnnotations.push(annotation)
1930
2368
  if (annotation.type === "container_file_citation") {
@@ -2006,6 +2444,48 @@ const makeStreamResponse = Effect.fnUntraced(
2006
2444
  break
2007
2445
  }
2008
2446
 
2447
+ case "response.function_call_arguments.done": {
2448
+ const toolCall = activeToolCalls[event.output_index]
2449
+ if (
2450
+ Predicate.isNotUndefined(toolCall?.functionCall) &&
2451
+ !toolCall.functionCall.emitted
2452
+ ) {
2453
+ hasToolCalls = true
2454
+
2455
+ const toolParams = yield* Effect.try({
2456
+ try: () => Tool.unsafeSecureJsonParse(event.arguments),
2457
+ catch: (cause) =>
2458
+ AiError.make({
2459
+ module: "OpenAiLanguageModel",
2460
+ method: "makeStreamResponse",
2461
+ reason: new AiError.ToolParameterValidationError({
2462
+ toolName: toolCall.name,
2463
+ toolParams: {},
2464
+ description: `Failed securely JSON parse tool parameters: ${cause}`
2465
+ })
2466
+ })
2467
+ })
2468
+
2469
+ const params = yield* transformToolCallParams(options.tools, toolCall.name, toolParams)
2470
+
2471
+ parts.push({
2472
+ type: "tool-params-end",
2473
+ id: toolCall.id
2474
+ })
2475
+
2476
+ parts.push({
2477
+ type: "tool-call",
2478
+ id: toolCall.id,
2479
+ name: toolCall.name,
2480
+ params,
2481
+ metadata: { openai: makeItemIdMetadata(event.item_id) }
2482
+ })
2483
+
2484
+ toolCall.functionCall.emitted = true
2485
+ }
2486
+ break
2487
+ }
2488
+
2009
2489
  case "response.apply_patch_call_operation_diff.delta": {
2010
2490
  const toolCall = activeToolCalls[event.output_index]
2011
2491
  if (Predicate.isNotUndefined(toolCall?.applyPatch)) {
@@ -2095,28 +2575,28 @@ const makeStreamResponse = Effect.fnUntraced(
2095
2575
  }
2096
2576
 
2097
2577
  case "response.reasoning_summary_part.added": {
2098
- // The first reasoning start is pushed in the `response.output_item.added` block
2578
+ const reasoningPart = getOrCreateReasoningPart(event.item_id)
2099
2579
  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
- }
2580
+ // Conclude all can-conclude parts before starting new one
2581
+ for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
2582
+ if (status === "can-conclude") {
2583
+ parts.push({
2584
+ type: "reasoning-end",
2585
+ id: `${event.item_id}:${summaryIndex}`,
2586
+ metadata: {
2587
+ openai: {
2588
+ ...makeItemIdMetadata(event.item_id),
2589
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
2113
2590
  }
2114
- })
2115
- reasoningPart.summaryParts[Number(summaryIndex)] = "concluded"
2116
- }
2591
+ }
2592
+ })
2593
+ reasoningPart.summaryParts[Number(summaryIndex)] = "concluded"
2117
2594
  }
2118
- reasoningPart.summaryParts[event.summary_index] = "active"
2119
2595
  }
2596
+ }
2597
+
2598
+ if (Predicate.isUndefined(reasoningPart.summaryParts[event.summary_index])) {
2599
+ reasoningPart.summaryParts[event.summary_index] = "active"
2120
2600
  parts.push({
2121
2601
  type: "reasoning-start",
2122
2602
  id: `${event.item_id}:${event.summary_index}`,
@@ -2136,26 +2616,27 @@ const makeStreamResponse = Effect.fnUntraced(
2136
2616
  type: "reasoning-delta",
2137
2617
  id: `${event.item_id}:${event.summary_index}`,
2138
2618
  delta: event.delta,
2139
- metadata: { openai: { ...makeItemIdMetadata(event.item_id) } }
2619
+ metadata: { openai: makeItemIdMetadata(event.item_id) }
2140
2620
  })
2141
2621
  break
2142
2622
  }
2143
2623
 
2144
2624
  case "response.reasoning_summary_part.done": {
2625
+ const reasoningPart = getOrCreateReasoningPart(event.item_id)
2145
2626
  // When OpenAI stores message data, we can immediately conclude the
2146
2627
  // reasoning part given that we do not need the encrypted content
2147
2628
  if (config.store === true) {
2148
2629
  parts.push({
2149
2630
  type: "reasoning-end",
2150
2631
  id: `${event.item_id}:${event.summary_index}`,
2151
- metadata: { openai: { ...makeItemIdMetadata(event.item_id) } }
2632
+ metadata: { openai: makeItemIdMetadata(event.item_id) }
2152
2633
  })
2153
2634
  // Mark the summary part concluded
2154
- activeReasoning[event.item_id].summaryParts[event.summary_index] = "concluded"
2635
+ reasoningPart.summaryParts[event.summary_index] = "concluded"
2155
2636
  } else {
2156
2637
  // Mark the summary part as can-conclude given we still need a
2157
2638
  // final summary part with the encrypted content
2158
- activeReasoning[event.item_id].summaryParts[event.summary_index] = "can-conclude"
2639
+ reasoningPart.summaryParts[event.summary_index] = "can-conclude"
2159
2640
  }
2160
2641
  break
2161
2642
  }
@@ -2174,7 +2655,7 @@ const makeStreamResponse = Effect.fnUntraced(
2174
2655
 
2175
2656
  const annotateRequest = (
2176
2657
  span: Span,
2177
- request: typeof Generated.CreateResponse.Encoded
2658
+ request: typeof OpenAiSchema.CreateResponse.Encoded
2178
2659
  ): void => {
2179
2660
  addGenAIAnnotations(span, {
2180
2661
  system: "openai",
@@ -2194,7 +2675,7 @@ const annotateRequest = (
2194
2675
  })
2195
2676
  }
2196
2677
 
2197
- const annotateResponse = (span: Span, response: Generated.Response): void => {
2678
+ const annotateResponse = (span: Span, response: OpenAiSchema.Response): void => {
2198
2679
  const finishReason = response.incomplete_details?.reason as string | undefined
2199
2680
  addGenAIAnnotations(span, {
2200
2681
  response: {
@@ -2244,7 +2725,7 @@ const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) =>
2244
2725
  // Tool Conversion
2245
2726
  // =============================================================================
2246
2727
 
2247
- type OpenAiToolChoice = typeof Generated.CreateResponse.Encoded["tool_choice"]
2728
+ type OpenAiToolChoice = typeof OpenAiSchema.CreateResponse.Encoded["tool_choice"]
2248
2729
 
2249
2730
  const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>({
2250
2731
  config,
@@ -2255,7 +2736,7 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2255
2736
  readonly options: LanguageModel.ProviderOptions
2256
2737
  readonly toolNameMapper: Tool.NameMapper<Tools>
2257
2738
  }): Effect.fn.Return<{
2258
- readonly tools: ReadonlyArray<typeof Generated.Tool.Encoded> | undefined
2739
+ readonly tools: ReadonlyArray<typeof OpenAiSchema.Tool.Encoded> | undefined
2259
2740
  readonly toolChoice: OpenAiToolChoice | undefined
2260
2741
  }, AiError.AiError> {
2261
2742
  // Return immediately if no tools are in the toolkit
@@ -2263,7 +2744,7 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2263
2744
  return { tools: undefined, toolChoice: undefined }
2264
2745
  }
2265
2746
 
2266
- const tools: Array<typeof Generated.Tool.Encoded> = []
2747
+ const tools: Array<typeof OpenAiSchema.Tool.Encoded> = []
2267
2748
  let toolChoice: OpenAiToolChoice | undefined = undefined
2268
2749
 
2269
2750
  // Filter the incoming tools down to the set of allowed tools as indicated by
@@ -2279,14 +2760,16 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2279
2760
 
2280
2761
  // Convert the tools in the toolkit to the provider-defined format
2281
2762
  for (const tool of allowedTools) {
2282
- if (Tool.isUserDefined(tool)) {
2763
+ if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) {
2283
2764
  const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true
2765
+ const description = Tool.getDescription(tool)
2766
+ const parameters = yield* tryToolJsonSchema(tool, "prepareTools")
2284
2767
  tools.push({
2285
2768
  type: "function",
2286
2769
  name: tool.name,
2287
- description: Tool.getDescription(tool) ?? null,
2288
- parameters: Tool.getJsonSchema(tool) as { readonly [x: string]: Schema.Json },
2289
- strict
2770
+ parameters,
2771
+ strict,
2772
+ ...(Predicate.isNotUndefined(description) ? { description } : undefined)
2290
2773
  })
2291
2774
  }
2292
2775
 
@@ -2468,35 +2951,63 @@ const getStatus = (
2468
2951
  | Prompt.TextPart
2469
2952
  | Prompt.ToolCallPart
2470
2953
  | Prompt.ToolResultPart
2471
- ): typeof Generated.Message.Encoded["status"] | null => part.options.openai?.status ?? null
2954
+ ): typeof OpenAiSchema.MessageStatus.Encoded | null => part.options.openai?.status ?? null
2472
2955
  const getEncryptedContent = (
2473
2956
  part: Prompt.ReasoningPart
2474
2957
  ): string | null => part.options.openai?.encryptedContent ?? null
2475
2958
 
2476
2959
  const getImageDetail = (part: Prompt.FilePart): ImageDetail => part.options.openai?.imageDetail ?? "auto"
2477
2960
 
2478
- const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefined(itemId) ? { itemId } : undefined
2961
+ const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefined(itemId) ? { itemId } : {}
2479
2962
 
2480
2963
  const makeEncryptedContentMetadata = (encryptedContent: string | null | undefined) =>
2481
2964
  Predicate.isNotNullish(encryptedContent) ? { encryptedContent } : undefined
2482
2965
 
2483
- const prepareResponseFormat = ({ config, options }: {
2966
+ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError =>
2967
+ AiError.make({
2968
+ module: "OpenAiLanguageModel",
2969
+ method,
2970
+ reason: new AiError.UnsupportedSchemaError({
2971
+ description: error instanceof Error ? error.message : String(error)
2972
+ })
2973
+ })
2974
+
2975
+ const tryCodecTransform = <S extends Schema.Top>(schema: S, method: string) =>
2976
+ Effect.try({
2977
+ try: () => toCodecOpenAI(schema),
2978
+ catch: (error) => unsupportedSchemaError(error, method)
2979
+ })
2980
+
2981
+ const tryJsonSchema = <S extends Schema.Top>(schema: S, method: string) =>
2982
+ Effect.try({
2983
+ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }),
2984
+ catch: (error) => unsupportedSchemaError(error, method)
2985
+ })
2986
+
2987
+ const tryToolJsonSchema = <T extends Tool.Any>(tool: T, method: string) =>
2988
+ Effect.try({
2989
+ try: () => Tool.getJsonSchema(tool, { transformer: toCodecOpenAI }),
2990
+ catch: (error) => unsupportedSchemaError(error, method)
2991
+ })
2992
+
2993
+ const prepareResponseFormat = Effect.fnUntraced(function*({ config, options }: {
2484
2994
  readonly config: typeof Config.Service
2485
2995
  readonly options: LanguageModel.ProviderOptions
2486
- }): typeof Generated.TextResponseFormatConfiguration.Encoded => {
2996
+ }): Effect.fn.Return<typeof OpenAiSchema.TextResponseFormatConfiguration.Encoded, AiError.AiError> {
2487
2997
  if (options.responseFormat.type === "json") {
2488
2998
  const name = options.responseFormat.objectName
2489
2999
  const schema = options.responseFormat.schema
3000
+ const jsonSchema = yield* tryJsonSchema(schema, "prepareResponseFormat")
2490
3001
  return {
2491
3002
  type: "json_schema",
2492
3003
  name,
2493
3004
  description: AST.resolveDescription(schema.ast) ?? "Response with a JSON object",
2494
- schema: Tool.getJsonSchemaFromSchema(schema) as any,
3005
+ schema: jsonSchema,
2495
3006
  strict: config.strictJsonSchema ?? true
2496
3007
  }
2497
3008
  }
2498
3009
  return { type: "text" }
2499
- }
3010
+ })
2500
3011
 
2501
3012
  interface ModelCapabilities {
2502
3013
  readonly isReasoningModel: boolean
@@ -2570,7 +3081,42 @@ const getApprovalRequestIdMapping = (prompt: Prompt.Prompt): ReadonlyMap<string,
2570
3081
  return mapping
2571
3082
  }
2572
3083
 
2573
- const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.Usage => {
3084
+ const normalizeMcpToolCall = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>({
3085
+ toolNameMapper,
3086
+ toolParams,
3087
+ method
3088
+ }: {
3089
+ readonly toolNameMapper: Tool.NameMapper<Tools>
3090
+ readonly toolParams: unknown
3091
+ readonly method: string
3092
+ }): Effect.fn.Return<{
3093
+ readonly toolName: string
3094
+ readonly params: unknown
3095
+ }, AiError.AiError> {
3096
+ const toolName = toolNameMapper.getCustomName("mcp")
3097
+
3098
+ if (typeof toolParams !== "string") {
3099
+ return { toolName, params: toolParams }
3100
+ }
3101
+
3102
+ const params = yield* Effect.try({
3103
+ try: () => Tool.unsafeSecureJsonParse(toolParams),
3104
+ catch: (cause) =>
3105
+ AiError.make({
3106
+ module: "OpenAiLanguageModel",
3107
+ method,
3108
+ reason: new AiError.ToolParameterValidationError({
3109
+ toolName,
3110
+ toolParams,
3111
+ description: `Failed to securely JSON parse tool parameters: ${cause}`
3112
+ })
3113
+ })
3114
+ })
3115
+
3116
+ return { toolName, params }
3117
+ })
3118
+
3119
+ const getUsage = (usage: OpenAiSchema.ResponseUsage | null | undefined): Response.Usage => {
2574
3120
  if (Predicate.isNullish(usage)) {
2575
3121
  return {
2576
3122
  inputTokens: {
@@ -2589,8 +3135,8 @@ const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.U
2589
3135
 
2590
3136
  const inputTokens = usage.input_tokens
2591
3137
  const outputTokens = usage.output_tokens
2592
- const cachedTokens = usage.input_tokens_details.cached_tokens
2593
- const reasoningTokens = usage.output_tokens_details.reasoning_tokens
3138
+ const cachedTokens = getUsageTokenDetail(usage.input_tokens_details, "cached_tokens")
3139
+ const reasoningTokens = getUsageTokenDetail(usage.output_tokens_details, "reasoning_tokens")
2594
3140
 
2595
3141
  return {
2596
3142
  inputTokens: {
@@ -2606,3 +3152,64 @@ const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.U
2606
3152
  }
2607
3153
  }
2608
3154
  }
3155
+
3156
+ type ServiceTier = "default" | "auto" | "flex" | "scale" | "priority" | null
3157
+
3158
+ const toServiceTier = (value: string | undefined): {
3159
+ readonly metadata: {
3160
+ readonly openai: {
3161
+ readonly serviceTier: ServiceTier
3162
+ }
3163
+ }
3164
+ } | undefined => {
3165
+ switch (value) {
3166
+ case "default":
3167
+ case "auto":
3168
+ case "flex":
3169
+ case "scale":
3170
+ case "priority":
3171
+ return { metadata: { openai: { serviceTier: value } } }
3172
+ default:
3173
+ return undefined
3174
+ }
3175
+ }
3176
+
3177
+ const getUsageTokenDetail = (details: unknown, key: string): number =>
3178
+ Predicate.hasProperty(details, key) && typeof details[key] === "number" ? details[key] : 0
3179
+
3180
+ const transformToolCallParams = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>(
3181
+ tools: Tools,
3182
+ toolName: string,
3183
+ toolParams: unknown
3184
+ ): Effect.fn.Return<unknown, AiError.AiError> {
3185
+ const tool = tools.find((tool) => tool.name === toolName)
3186
+
3187
+ if (Predicate.isUndefined(tool)) {
3188
+ return yield* AiError.make({
3189
+ module: "OpenAiLanguageModel",
3190
+ method: "makeResponse",
3191
+ reason: new AiError.ToolNotFoundError({
3192
+ toolName,
3193
+ availableTools: tools.map((tool) => tool.name)
3194
+ })
3195
+ })
3196
+ }
3197
+
3198
+ const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse")
3199
+
3200
+ const transform = Schema.decodeEffect(codec)
3201
+
3202
+ return yield* (
3203
+ transform(toolParams) as Effect.Effect<unknown, Schema.SchemaError>
3204
+ ).pipe(Effect.mapError((error) =>
3205
+ AiError.make({
3206
+ module: "OpenAiLanguageModel",
3207
+ method: "makeResponse",
3208
+ reason: new AiError.ToolParameterValidationError({
3209
+ toolName,
3210
+ toolParams,
3211
+ description: error.issue.toString()
3212
+ })
3213
+ })
3214
+ ))
3215
+ })