@effect/ai-openai-compat 4.0.0-beta.9 → 4.0.0-beta.91

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 (41) hide show
  1. package/dist/OpenAiClient.d.ts +264 -52
  2. package/dist/OpenAiClient.d.ts.map +1 -1
  3. package/dist/OpenAiClient.js +97 -9
  4. package/dist/OpenAiClient.js.map +1 -1
  5. package/dist/OpenAiConfig.d.ts +68 -10
  6. package/dist/OpenAiConfig.d.ts.map +1 -1
  7. package/dist/OpenAiConfig.js +36 -7
  8. package/dist/OpenAiConfig.js.map +1 -1
  9. package/dist/OpenAiEmbeddingModel.d.ts +186 -0
  10. package/dist/OpenAiEmbeddingModel.d.ts.map +1 -0
  11. package/dist/OpenAiEmbeddingModel.js +190 -0
  12. package/dist/OpenAiEmbeddingModel.js.map +1 -0
  13. package/dist/OpenAiError.d.ts +109 -35
  14. package/dist/OpenAiError.d.ts.map +1 -1
  15. package/dist/OpenAiError.js +14 -1
  16. package/dist/OpenAiLanguageModel.d.ts +304 -20
  17. package/dist/OpenAiLanguageModel.d.ts.map +1 -1
  18. package/dist/OpenAiLanguageModel.js +157 -27
  19. package/dist/OpenAiLanguageModel.js.map +1 -1
  20. package/dist/OpenAiTelemetry.d.ts +76 -26
  21. package/dist/OpenAiTelemetry.d.ts.map +1 -1
  22. package/dist/OpenAiTelemetry.js +22 -10
  23. package/dist/OpenAiTelemetry.js.map +1 -1
  24. package/dist/index.d.ts +10 -17
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +10 -17
  27. package/dist/index.js.map +1 -1
  28. package/dist/internal/errors.js +4 -4
  29. package/dist/internal/errors.js.map +1 -1
  30. package/dist/internal/utilities.js +0 -6
  31. package/dist/internal/utilities.js.map +1 -1
  32. package/package.json +3 -3
  33. package/src/OpenAiClient.ts +273 -50
  34. package/src/OpenAiConfig.ts +69 -11
  35. package/src/OpenAiEmbeddingModel.ts +332 -0
  36. package/src/OpenAiError.ts +111 -35
  37. package/src/OpenAiLanguageModel.ts +426 -43
  38. package/src/OpenAiTelemetry.ts +81 -32
  39. package/src/index.ts +11 -17
  40. package/src/internal/errors.ts +4 -4
  41. package/src/internal/utilities.ts +0 -9
@@ -1,21 +1,24 @@
1
1
  /**
2
- * OpenAI Language Model implementation.
2
+ * The `OpenAiLanguageModel` module adapts OpenAI-compatible chat completions
3
+ * providers to Effect AI's `LanguageModel` service. It builds a model service
4
+ * from a model id, translates prompts, files, tools, structured output schemas,
5
+ * and provider-specific options into `OpenAiClient` requests, and maps normal
6
+ * or streaming chat completion results back into Effect AI response content and
7
+ * metadata.
3
8
  *
4
- * Provides a LanguageModel implementation for OpenAI's chat completions 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 type * 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
24
  import type { DeepMutable, Simplify } from "effect/Types"
@@ -58,12 +61,20 @@ type ImageDetail = "auto" | "low" | "high"
58
61
  // =============================================================================
59
62
 
60
63
  /**
61
- * Service definition for OpenAI language model configuration.
64
+ * Context service for OpenAI language model configuration.
65
+ *
66
+ * **When to use**
67
+ *
68
+ * Use as the context service for OpenAI-compatible language model request
69
+ * configuration, especially when a scoped operation should override the defaults
70
+ * supplied to `model`, `make`, or `layer`.
71
+ *
72
+ * @see {@link withConfigOverride} for scoping language model request overrides
62
73
  *
63
- * @since 1.0.0
64
74
  * @category context
75
+ * @since 4.0.0
65
76
  */
66
- export class Config extends ServiceMap.Service<
77
+ export class Config extends Context.Service<
67
78
  Config,
68
79
  Simplify<
69
80
  & Partial<
@@ -101,6 +112,7 @@ export class Config extends ServiceMap.Service<
101
112
  * Defaults to `true`.
102
113
  */
103
114
  readonly strictJsonSchema?: boolean | undefined
115
+ readonly [x: string]: unknown
104
116
  }
105
117
  >
106
118
  >()("@effect/ai-openai-compat/OpenAiLanguageModel/Config") {}
@@ -110,7 +122,16 @@ export class Config extends ServiceMap.Service<
110
122
  // =============================================================================
111
123
 
112
124
  declare module "effect/unstable/ai/Prompt" {
125
+ /**
126
+ * OpenAI-compatible options for file prompt parts.
127
+ *
128
+ * @category request
129
+ * @since 4.0.0
130
+ */
113
131
  export interface FilePartOptions extends ProviderOptions {
132
+ /**
133
+ * Provider-specific file options for OpenAI-compatible APIs.
134
+ */
114
135
  readonly openai?: {
115
136
  /**
116
137
  * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`.
@@ -119,7 +140,16 @@ declare module "effect/unstable/ai/Prompt" {
119
140
  } | null
120
141
  }
121
142
 
143
+ /**
144
+ * OpenAI-compatible options for reasoning prompt parts.
145
+ *
146
+ * @category request
147
+ * @since 4.0.0
148
+ */
122
149
  export interface ReasoningPartOptions extends ProviderOptions {
150
+ /**
151
+ * Provider-specific reasoning options for OpenAI-compatible APIs.
152
+ */
123
153
  readonly openai?: {
124
154
  /**
125
155
  * The ID of the item to reference.
@@ -134,40 +164,67 @@ declare module "effect/unstable/ai/Prompt" {
134
164
  } | null
135
165
  }
136
166
 
167
+ /**
168
+ * OpenAI-compatible options for assistant tool-call prompt parts.
169
+ *
170
+ * @category request
171
+ * @since 4.0.0
172
+ */
137
173
  export interface ToolCallPartOptions extends ProviderOptions {
174
+ /**
175
+ * Provider-specific tool-call options for OpenAI-compatible APIs.
176
+ */
138
177
  readonly openai?: {
139
178
  /**
140
179
  * The ID of the item to reference.
141
180
  */
142
181
  readonly itemId?: string | null
143
182
  /**
144
- * The status of item.
183
+ * The status to send for the tool-call item.
145
184
  */
146
185
  readonly status?: MessageStatus | null
147
186
  } | null
148
187
  }
149
188
 
189
+ /**
190
+ * OpenAI-compatible options for tool-result prompt parts.
191
+ *
192
+ * @category request
193
+ * @since 4.0.0
194
+ */
150
195
  export interface ToolResultPartOptions extends ProviderOptions {
196
+ /**
197
+ * Provider-specific tool-result options for OpenAI-compatible APIs.
198
+ */
151
199
  readonly openai?: {
152
200
  /**
153
201
  * The ID of the item to reference.
154
202
  */
155
203
  readonly itemId?: string | null
156
204
  /**
157
- * The status of item.
205
+ * The status to send for the tool-result item.
158
206
  */
159
207
  readonly status?: MessageStatus | null
160
208
  } | null
161
209
  }
162
210
 
211
+ /**
212
+ * OpenAI-compatible options for text prompt parts.
213
+ *
214
+ * @category request
215
+ * @since 4.0.0
216
+ */
163
217
  export interface TextPartOptions extends ProviderOptions {
218
+ /**
219
+ * Provider-specific text options for OpenAI-compatible APIs.
220
+ */
164
221
  readonly openai?: {
165
222
  /**
166
223
  * The ID of the item to reference.
167
224
  */
168
225
  readonly itemId?: string | null
169
226
  /**
170
- * The status of item.
227
+ * The status to send for the text item.
171
228
  */
172
229
  readonly status?: MessageStatus | null
173
230
  /**
@@ -179,8 +236,20 @@ declare module "effect/unstable/ai/Prompt" {
179
236
  }
180
237
 
181
238
  declare module "effect/unstable/ai/Response" {
239
+ /**
240
+ * OpenAI-compatible metadata attached to a complete text response part.
241
+ *
242
+ * @category response
243
+ * @since 4.0.0
244
+ */
182
245
  export interface TextPartMetadata extends ProviderMetadata {
246
+ /**
247
+ * Provider-specific metadata returned for the text part.
248
+ */
183
249
  readonly openai?: {
250
+ /**
251
+ * The OpenAI item ID associated with the text part.
252
+ */
184
253
  readonly itemId?: string | null
185
254
  /**
186
255
  * If the model emits a refusal content part, the refusal explanation
@@ -189,7 +258,7 @@ declare module "effect/unstable/ai/Response" {
189
258
  */
190
259
  readonly refusal?: string | null
191
260
  /**
192
- * The status of item.
261
+ * The status returned for the text item.
193
262
  */
194
263
  readonly status?: MessageStatus | null
195
264
  /**
@@ -199,55 +268,163 @@ declare module "effect/unstable/ai/Response" {
199
268
  }
200
269
  }
201
270
 
271
+ /**
272
+ * OpenAI-compatible metadata emitted when a streamed text part starts.
273
+ *
274
+ * @category response
275
+ * @since 4.0.0
276
+ */
202
277
  export interface TextStartPartMetadata extends ProviderMetadata {
278
+ /**
279
+ * Provider-specific metadata returned for the streamed text start.
280
+ */
203
281
  readonly openai?: {
282
+ /**
283
+ * The OpenAI item ID associated with the streamed text part.
284
+ */
204
285
  readonly itemId?: string | null
205
286
  } | null
206
287
  }
207
288
 
289
+ /**
290
+ * OpenAI-compatible metadata emitted when a streamed text part ends.
291
+ *
292
+ * @category response
293
+ * @since 4.0.0
294
+ */
208
295
  export interface TextEndPartMetadata extends ProviderMetadata {
296
+ /**
297
+ * Provider-specific metadata returned for the streamed text end.
298
+ */
209
299
  readonly openai?: {
300
+ /**
301
+ * The OpenAI item ID associated with the streamed text part.
302
+ */
210
303
  readonly itemId?: string | null
304
+ /**
305
+ * The annotations collected for the completed streamed text part.
306
+ */
211
307
  readonly annotations?: ReadonlyArray<Annotation> | null
212
308
  } | null
213
309
  }
214
310
 
311
+ /**
312
+ * OpenAI-compatible metadata attached to a complete reasoning response part.
313
+ *
314
+ * @category response
315
+ * @since 4.0.0
316
+ */
215
317
  export interface ReasoningPartMetadata extends ProviderMetadata {
318
+ /**
319
+ * Provider-specific metadata returned for the reasoning part.
320
+ */
216
321
  readonly openai?: {
322
+ /**
323
+ * The OpenAI item ID associated with the reasoning part.
324
+ */
217
325
  readonly itemId?: string | null
326
+ /**
327
+ * Encrypted reasoning content that can be sent back in later requests.
328
+ */
218
329
  readonly encryptedContent?: string | null
219
330
  } | null
220
331
  }
221
332
 
333
+ /**
334
+ * OpenAI-compatible metadata emitted when a streamed reasoning part starts.
335
+ *
336
+ * @category response
337
+ * @since 4.0.0
338
+ */
222
339
  export interface ReasoningStartPartMetadata extends ProviderMetadata {
340
+ /**
341
+ * Provider-specific metadata returned for the streamed reasoning start.
342
+ */
223
343
  readonly openai?: {
344
+ /**
345
+ * The OpenAI item ID associated with the reasoning part.
346
+ */
224
347
  readonly itemId?: string | null
348
+ /**
349
+ * Encrypted reasoning content that can be sent back in later requests.
350
+ */
225
351
  readonly encryptedContent?: string | null
226
352
  } | null
227
353
  }
228
354
 
355
+ /**
356
+ * OpenAI-compatible metadata emitted for a streamed reasoning delta.
357
+ *
358
+ * @category response
359
+ * @since 4.0.0
360
+ */
229
361
  export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
362
+ /**
363
+ * Provider-specific metadata returned for the streamed reasoning delta.
364
+ */
230
365
  readonly openai?: {
366
+ /**
367
+ * The OpenAI item ID associated with the reasoning part.
368
+ */
231
369
  readonly itemId?: string | null
232
370
  } | null
233
371
  }
234
372
 
373
+ /**
374
+ * OpenAI-compatible metadata emitted when a streamed reasoning part ends.
375
+ *
376
+ * @category response
377
+ * @since 4.0.0
378
+ */
235
379
  export interface ReasoningEndPartMetadata extends ProviderMetadata {
380
+ /**
381
+ * Provider-specific metadata returned for the streamed reasoning end.
382
+ */
236
383
  readonly openai?: {
384
+ /**
385
+ * The OpenAI item ID associated with the reasoning part.
386
+ */
237
387
  readonly itemId?: string | null
388
+ /**
389
+ * Encrypted reasoning content that can be sent back in later requests.
390
+ */
238
391
  readonly encryptedContent?: string
239
392
  } | null
240
393
  }
241
394
 
395
+ /**
396
+ * OpenAI-compatible metadata attached to tool-call response parts.
397
+ *
398
+ * @category response
399
+ * @since 4.0.0
400
+ */
242
401
  export interface ToolCallPartMetadata extends ProviderMetadata {
402
+ /**
403
+ * Provider-specific metadata returned for the tool call.
404
+ */
243
405
  readonly openai?: {
406
+ /**
407
+ * The OpenAI item ID associated with the tool call.
408
+ */
244
409
  readonly itemId?: string | null
245
410
  } | null
246
411
  }
247
412
 
413
+ /**
414
+ * OpenAI-compatible metadata attached to document source citations.
415
+ *
416
+ * @category response
417
+ * @since 4.0.0
418
+ */
248
419
  export interface DocumentSourcePartMetadata extends ProviderMetadata {
420
+ /**
421
+ * Provider-specific citation metadata for OpenAI-compatible APIs.
422
+ */
249
423
  readonly openai?:
250
424
  | {
425
+ /**
426
+ * Identifies a citation to an uploaded file.
427
+ */
251
428
  readonly type: "file_citation"
252
429
  /**
253
430
  * The index of the file in the list of files.
@@ -259,6 +436,9 @@ declare module "effect/unstable/ai/Response" {
259
436
  readonly fileId: string
260
437
  }
261
438
  | {
439
+ /**
440
+ * Identifies a citation to a generated file path.
441
+ */
262
442
  readonly type: "file_path"
263
443
  /**
264
444
  * The index of the file in the list of files.
@@ -270,6 +450,9 @@ declare module "effect/unstable/ai/Response" {
270
450
  readonly fileId: string
271
451
  }
272
452
  | {
453
+ /**
454
+ * Identifies a citation to a file inside a container.
455
+ */
273
456
  readonly type: "container_file_citation"
274
457
  /**
275
458
  * The ID of the file.
@@ -283,8 +466,20 @@ declare module "effect/unstable/ai/Response" {
283
466
  | null
284
467
  }
285
468
 
469
+ /**
470
+ * OpenAI-compatible metadata attached to URL source citations.
471
+ *
472
+ * @category response
473
+ * @since 4.0.0
474
+ */
286
475
  export interface UrlSourcePartMetadata extends ProviderMetadata {
476
+ /**
477
+ * Provider-specific URL citation metadata for OpenAI-compatible APIs.
478
+ */
287
479
  readonly openai?: {
480
+ /**
481
+ * Identifies a citation to a URL.
482
+ */
288
483
  readonly type: "url_citation"
289
484
  /**
290
485
  * The index of the first character of the URL citation in the message.
@@ -297,8 +492,20 @@ declare module "effect/unstable/ai/Response" {
297
492
  } | null
298
493
  }
299
494
 
495
+ /**
496
+ * OpenAI-compatible metadata attached to finish response parts.
497
+ *
498
+ * @category response
499
+ * @since 4.0.0
500
+ */
300
501
  export interface FinishPartMetadata extends ProviderMetadata {
502
+ /**
503
+ * Provider-specific metadata returned when generation finishes.
504
+ */
301
505
  readonly openai?: {
506
+ /**
507
+ * The service tier reported by the OpenAI-compatible provider.
508
+ */
302
509
  readonly serviceTier?: "default" | "auto" | "flex" | "scale" | "priority" | null
303
510
  } | null
304
511
  }
@@ -309,31 +516,56 @@ declare module "effect/unstable/ai/Response" {
309
516
  // =============================================================================
310
517
 
311
518
  /**
312
- * @since 1.0.0
519
+ * Creates an OpenAI-compatible model descriptor that can be provided with `Effect.provide`.
520
+ *
521
+ * **When to use**
522
+ *
523
+ * Use when you want an OpenAI-compatible language model value that carries
524
+ * provider and model metadata and can be supplied directly to an Effect program.
525
+ *
526
+ * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly
527
+ * @see {@link make} for constructing the language model service effectfully
528
+ *
313
529
  * @category constructors
530
+ * @since 4.0.0
314
531
  */
315
532
  export const model = (
316
533
  model: string,
317
534
  config?: Omit<typeof Config.Service, "model">
318
535
  ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> =>
319
- AiModel.make("openai", layer({ model, config }))
536
+ AiModel.make("openai", model, layer({ model, config }))
320
537
 
321
538
  // TODO
322
539
  // /**
323
- // * @since 1.0.0
540
+ // * @since 4.0.0
324
541
  // * @category constructors
325
542
  // */
326
543
  // export const modelWithTokenizer = (
327
544
  // model: string,
328
545
  // config?: Omit<typeof Config.Service, "model">
329
546
  // ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
330
- // AiModel.make("openai", layerWithTokenizer({ model, config }))
547
+ // AiModel.make("openai", model, layerWithTokenizer({ model, config }))
331
548
 
332
549
  /**
333
- * Creates an OpenAI language model service.
550
+ * Creates an OpenAI-compatible `LanguageModel` service from a model identifier and optional request defaults.
551
+ *
552
+ * **When to use**
553
+ *
554
+ * Use to construct an OpenAI-compatible chat-completions language model service
555
+ * backed by `OpenAiClient`.
556
+ *
557
+ * **Details**
558
+ *
559
+ * The returned effect requires `OpenAiClient`. Request defaults from the
560
+ * `config` option are merged with any `Config` service in the context, with
561
+ * context values taking precedence. The service supports both `generateText` and
562
+ * `streamText`.
563
+ *
564
+ * @see {@link layer} for providing the service as a `Layer`
565
+ * @see {@link model} for creating a model descriptor for `AiModel.provide`
334
566
  *
335
- * @since 1.0.0
336
567
  * @category constructors
568
+ * @since 4.0.0
337
569
  */
338
570
  export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
339
571
  readonly model: string
@@ -342,7 +574,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
342
574
  const client = yield* OpenAiClient
343
575
 
344
576
  const makeConfig = Effect.gen(function*() {
345
- const services = yield* Effect.services<never>()
577
+ const services = yield* Effect.context<never>()
346
578
  return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
347
579
  })
348
580
 
@@ -370,8 +602,9 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
370
602
  config,
371
603
  options
372
604
  })
605
+ const { fileIdPrefixes: _fip, strictJsonSchema: _sjs, ...apiConfig } = config
373
606
  const request: CreateResponse = {
374
- ...config,
607
+ ...apiConfig,
375
608
  input: messages,
376
609
  include: include.size > 0 ? Array.from(include) : null,
377
610
  text: {
@@ -386,6 +619,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
386
619
  )
387
620
 
388
621
  return yield* LanguageModel.make({
622
+ codecTransformer: toCodecOpenAI,
389
623
  generateText: Effect.fnUntraced(
390
624
  function*(options) {
391
625
  const config = yield* makeConfig
@@ -423,17 +657,23 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
423
657
  })
424
658
  )
425
659
  )
426
- }).pipe(Effect.provideService(
427
- LanguageModel.CurrentCodecTransformer,
428
- toCodecOpenAI
429
- ))
660
+ })
430
661
  })
431
662
 
432
663
  /**
433
- * Creates a layer for the OpenAI language model.
664
+ * Creates a layer for the OpenAI-compatible language model.
665
+ *
666
+ * **When to use**
667
+ *
668
+ * Use when composing application layers and you want OpenAI-compatible APIs to
669
+ * satisfy `LanguageModel.LanguageModel` while supplying `OpenAiClient` from
670
+ * another layer.
671
+ *
672
+ * @see {@link make} for constructing the language model service effectfully
673
+ * @see {@link model} for creating an AI model descriptor
434
674
  *
435
- * @since 1.0.0
436
675
  * @category layers
676
+ * @since 4.0.0
437
677
  */
438
678
  export const layer = (options: {
439
679
  readonly model: string
@@ -442,39 +682,99 @@ export const layer = (options: {
442
682
  Layer.effect(LanguageModel.LanguageModel, make(options))
443
683
 
444
684
  /**
445
- * Provides config overrides for OpenAI language model operations.
685
+ * Provides scoped config overrides for OpenAI-compatible language model operations.
686
+ *
687
+ * **When to use**
688
+ *
689
+ * Use to override request configuration for a single language model effect
690
+ * without changing the defaults supplied to `model`, `make`, or `layer`.
691
+ *
692
+ * **Details**
693
+ *
694
+ * Existing `Config` values from the Effect context are merged with `overrides`,
695
+ * and the override values take precedence.
696
+ *
697
+ * @see {@link Config} for the configuration shape
446
698
  *
447
- * @since 1.0.0
448
699
  * @category configuration
700
+ * @since 4.0.0
449
701
  */
450
702
  export const withConfigOverride: {
451
703
  /**
452
- * Provides config overrides for OpenAI language model operations.
704
+ * Provides scoped config overrides for OpenAI-compatible language model operations.
705
+ *
706
+ * **When to use**
707
+ *
708
+ * Use to override request configuration for a single language model effect
709
+ * without changing the defaults supplied to `model`, `make`, or `layer`.
710
+ *
711
+ * **Details**
712
+ *
713
+ * Existing `Config` values from the Effect context are merged with `overrides`,
714
+ * and the override values take precedence.
715
+ *
716
+ * @see {@link Config} for the configuration shape
453
717
  *
454
- * @since 1.0.0
455
718
  * @category configuration
719
+ * @since 4.0.0
456
720
  */
457
721
  (overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
458
722
  /**
459
- * Provides config overrides for OpenAI language model operations.
723
+ * Provides scoped config overrides for OpenAI-compatible language model operations.
724
+ *
725
+ * **When to use**
726
+ *
727
+ * Use to override request configuration for a single language model effect
728
+ * without changing the defaults supplied to `model`, `make`, or `layer`.
729
+ *
730
+ * **Details**
731
+ *
732
+ * Existing `Config` values from the Effect context are merged with `overrides`,
733
+ * and the override values take precedence.
734
+ *
735
+ * @see {@link Config} for the configuration shape
460
736
  *
461
- * @since 1.0.0
462
737
  * @category configuration
738
+ * @since 4.0.0
463
739
  */
464
740
  <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
465
741
  } = dual<
466
742
  /**
467
- * Provides config overrides for OpenAI language model operations.
743
+ * Provides scoped config overrides for OpenAI-compatible language model operations.
744
+ *
745
+ * **When to use**
746
+ *
747
+ * Use to override request configuration for a single language model effect
748
+ * without changing the defaults supplied to `model`, `make`, or `layer`.
749
+ *
750
+ * **Details**
751
+ *
752
+ * Existing `Config` values from the Effect context are merged with `overrides`,
753
+ * and the override values take precedence.
754
+ *
755
+ * @see {@link Config} for the configuration shape
468
756
  *
469
- * @since 1.0.0
470
757
  * @category configuration
758
+ * @since 4.0.0
471
759
  */
472
760
  (overrides: typeof Config.Service) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
473
761
  /**
474
- * Provides config overrides for OpenAI language model operations.
762
+ * Provides scoped config overrides for OpenAI-compatible language model operations.
763
+ *
764
+ * **When to use**
765
+ *
766
+ * Use to override request configuration for a single language model effect
767
+ * without changing the defaults supplied to `model`, `make`, or `layer`.
768
+ *
769
+ * **Details**
770
+ *
771
+ * Existing `Config` values from the Effect context are merged with `overrides`,
772
+ * and the override values take precedence.
773
+ *
774
+ * @see {@link Config} for the configuration shape
475
775
  *
476
- * @since 1.0.0
477
776
  * @category configuration
777
+ * @since 4.0.0
478
778
  */
479
779
  <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
480
780
  >(2, (self, overrides) =>
@@ -782,7 +1082,7 @@ const buildHttpRequestDetails = (
782
1082
  method: request.method,
783
1083
  url: request.url,
784
1084
  urlParams: Array.from(request.urlParams),
785
- hash: request.hash,
1085
+ hash: Option.getOrUndefined(request.hash),
786
1086
  headers: Redactable.redact(request.headers) as Record<string, string>
787
1087
  })
788
1088
 
@@ -834,6 +1134,11 @@ const makeResponse = Effect.fnUntraced(
834
1134
  const message = choice?.message
835
1135
 
836
1136
  if (message !== undefined) {
1137
+ const reasoning = message.reasoning ?? message.reasoning_content
1138
+ if (Predicate.isNotNullish(reasoning) && reasoning.length > 0) {
1139
+ parts.push({ type: "reasoning", text: reasoning })
1140
+ }
1141
+
837
1142
  if (
838
1143
  message.content !== undefined && Predicate.isNotNull(message.content) && message.content.length > 0
839
1144
  ) {
@@ -907,6 +1212,8 @@ const makeStreamResponse = Effect.fnUntraced(
907
1212
  let metadataEmitted = false
908
1213
  let textStarted = false
909
1214
  let textId = ""
1215
+ let reasoningStarted = false
1216
+ let reasoningId = ""
910
1217
  let hasToolCalls = false
911
1218
  const activeToolCalls: Record<number, ActiveToolCall> = {}
912
1219
 
@@ -915,6 +1222,14 @@ const makeStreamResponse = Effect.fnUntraced(
915
1222
  const parts: Array<Response.StreamPartEncoded> = []
916
1223
 
917
1224
  if (event === "[DONE]") {
1225
+ if (reasoningStarted) {
1226
+ parts.push({
1227
+ type: "reasoning-end",
1228
+ id: reasoningId,
1229
+ metadata: { openai: { ...makeItemIdMetadata(reasoningId) } }
1230
+ })
1231
+ }
1232
+
918
1233
  if (textStarted) {
919
1234
  parts.push({
920
1235
  type: "text-end",
@@ -972,6 +1287,7 @@ const makeStreamResponse = Effect.fnUntraced(
972
1287
  if (!metadataEmitted) {
973
1288
  metadataEmitted = true
974
1289
  textId = `${event.id}_message`
1290
+ reasoningId = `${event.id}_reasoning`
975
1291
  parts.push({
976
1292
  type: "response-metadata",
977
1293
  id: event.id,
@@ -986,7 +1302,29 @@ const makeStreamResponse = Effect.fnUntraced(
986
1302
  return parts
987
1303
  }
988
1304
 
1305
+ const reasoningDelta = choice.delta?.reasoning ?? choice.delta?.reasoning_content
1306
+ if (Predicate.isNotNullish(reasoningDelta) && reasoningDelta.length > 0) {
1307
+ if (!reasoningStarted) {
1308
+ reasoningStarted = true
1309
+ parts.push({
1310
+ type: "reasoning-start",
1311
+ id: reasoningId,
1312
+ metadata: { openai: { ...makeItemIdMetadata(reasoningId) } }
1313
+ })
1314
+ }
1315
+ parts.push({ type: "reasoning-delta", id: reasoningId, delta: reasoningDelta })
1316
+ }
1317
+
989
1318
  if (choice.delta?.content !== undefined && Predicate.isNotNull(choice.delta.content)) {
1319
+ if (reasoningStarted) {
1320
+ reasoningStarted = false
1321
+ parts.push({
1322
+ type: "reasoning-end",
1323
+ id: reasoningId,
1324
+ metadata: { openai: { ...makeItemIdMetadata(reasoningId) } }
1325
+ })
1326
+ }
1327
+
990
1328
  if (!textStarted) {
991
1329
  textStarted = true
992
1330
  parts.push({
@@ -1002,11 +1340,13 @@ const makeStreamResponse = Effect.fnUntraced(
1002
1340
  hasToolCalls = hasToolCalls || choice.delta.tool_calls.length > 0
1003
1341
  choice.delta.tool_calls.forEach((deltaTool, indexInChunk) => {
1004
1342
  const toolIndex = deltaTool.index ?? indexInChunk
1005
- const toolId = deltaTool.id ?? `${event.id}_tool_${toolIndex}`
1343
+ const activeToolCall = activeToolCalls[toolIndex]
1344
+ const toolId = activeToolCall?.id ?? deltaTool.id ?? `${event.id}_tool_${toolIndex}`
1006
1345
  const providerToolName = deltaTool.function?.name
1007
- const toolName = toolNameMapper.getCustomName(providerToolName ?? "unknown_tool")
1346
+ const toolName = Predicate.isNotNullish(providerToolName)
1347
+ ? toolNameMapper.getCustomName(providerToolName)
1348
+ : activeToolCall?.name ?? toolNameMapper.getCustomName("unknown_tool")
1008
1349
  const argumentsDelta = deltaTool.function?.arguments ?? ""
1009
- const activeToolCall = activeToolCalls[toolIndex]
1010
1350
 
1011
1351
  if (Predicate.isUndefined(activeToolCall)) {
1012
1352
  activeToolCalls[toolIndex] = {
@@ -1124,7 +1464,7 @@ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError
1124
1464
  })
1125
1465
  })
1126
1466
 
1127
- const tryJsonSchema = <S extends Schema.Top>(schema: S, method: string) =>
1467
+ const tryJsonSchema = <S extends Schema.Constraint>(schema: S, method: string) =>
1128
1468
  Effect.try({
1129
1469
  try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }),
1130
1470
  catch: (error) => unsupportedSchemaError(error, method)
@@ -1169,7 +1509,7 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
1169
1509
 
1170
1510
  // Convert the tools in the toolkit to the provider-defined format
1171
1511
  for (const tool of allowedTools) {
1172
- if (Tool.isUserDefined(tool)) {
1512
+ if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) {
1173
1513
  const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true
1174
1514
  const parameters = yield* tryToolJsonSchema(tool, "prepareTools")
1175
1515
  tools.push({
@@ -1220,6 +1560,7 @@ const toChatCompletionsRequest = (payload: CreateResponse): CreateResponseReques
1220
1560
  const toolChoice = toChatToolChoice(payload.tool_choice)
1221
1561
 
1222
1562
  return {
1563
+ ...extractCustomRequestProperties(payload),
1223
1564
  model: payload.model ?? "",
1224
1565
  messages: messages.length > 0 ? messages : [{ role: "user", content: "" }],
1225
1566
  ...(payload.temperature !== undefined ? { temperature: payload.temperature } : undefined),
@@ -1231,12 +1572,54 @@ const toChatCompletionsRequest = (payload: CreateResponse): CreateResponseReques
1231
1572
  ? { parallel_tool_calls: payload.parallel_tool_calls }
1232
1573
  : undefined),
1233
1574
  ...(payload.service_tier !== undefined ? { service_tier: payload.service_tier } : undefined),
1575
+ ...(payload.reasoning !== undefined ? { reasoning: payload.reasoning } : undefined),
1234
1576
  ...(responseFormat !== undefined ? { response_format: responseFormat } : undefined),
1235
1577
  ...(tools.length > 0 ? { tools } : undefined),
1236
1578
  ...(toolChoice !== undefined ? { tool_choice: toolChoice } : undefined)
1237
1579
  }
1238
1580
  }
1239
1581
 
1582
+ const createResponseKnownProperties = new Set<string>([
1583
+ "metadata",
1584
+ "top_logprobs",
1585
+ "temperature",
1586
+ "top_p",
1587
+ "user",
1588
+ "safety_identifier",
1589
+ "prompt_cache_key",
1590
+ "service_tier",
1591
+ "prompt_cache_retention",
1592
+ "previous_response_id",
1593
+ "model",
1594
+ "reasoning",
1595
+ "background",
1596
+ "max_output_tokens",
1597
+ "max_tool_calls",
1598
+ "text",
1599
+ "tools",
1600
+ "tool_choice",
1601
+ "truncation",
1602
+ "input",
1603
+ "include",
1604
+ "parallel_tool_calls",
1605
+ "store",
1606
+ "instructions",
1607
+ "stream",
1608
+ "conversation",
1609
+ "modalities",
1610
+ "seed"
1611
+ ])
1612
+
1613
+ const extractCustomRequestProperties = (payload: CreateResponse): Record<string, unknown> => {
1614
+ const customProperties: Record<string, unknown> = {}
1615
+ for (const [key, value] of Object.entries(payload)) {
1616
+ if (!createResponseKnownProperties.has(key)) {
1617
+ customProperties[key] = value
1618
+ }
1619
+ }
1620
+ return customProperties
1621
+ }
1622
+
1240
1623
  const toChatResponseFormat = (
1241
1624
  format: TextResponseFormatConfiguration | undefined
1242
1625
  ): CreateResponseRequestJson["response_format"] | undefined => {