@effect/ai-openai 0.29.0 → 0.30.0

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 (62) hide show
  1. package/OpenAiTool/package.json +6 -0
  2. package/dist/cjs/Generated.js +5845 -4262
  3. package/dist/cjs/Generated.js.map +1 -1
  4. package/dist/cjs/OpenAiClient.js +1493 -129
  5. package/dist/cjs/OpenAiClient.js.map +1 -1
  6. package/dist/cjs/OpenAiEmbeddingModel.js +61 -50
  7. package/dist/cjs/OpenAiEmbeddingModel.js.map +1 -1
  8. package/dist/cjs/OpenAiLanguageModel.js +950 -326
  9. package/dist/cjs/OpenAiLanguageModel.js.map +1 -1
  10. package/dist/cjs/OpenAiTelemetry.js +4 -4
  11. package/dist/cjs/OpenAiTelemetry.js.map +1 -1
  12. package/dist/cjs/OpenAiTokenizer.js +46 -14
  13. package/dist/cjs/OpenAiTokenizer.js.map +1 -1
  14. package/dist/cjs/OpenAiTool.js +93 -0
  15. package/dist/cjs/OpenAiTool.js.map +1 -0
  16. package/dist/cjs/index.js +3 -1
  17. package/dist/cjs/internal/utilities.js +11 -3
  18. package/dist/cjs/internal/utilities.js.map +1 -1
  19. package/dist/dts/Generated.d.ts +19663 -11762
  20. package/dist/dts/Generated.d.ts.map +1 -1
  21. package/dist/dts/OpenAiClient.d.ts +3022 -14
  22. package/dist/dts/OpenAiClient.d.ts.map +1 -1
  23. package/dist/dts/OpenAiEmbeddingModel.d.ts +8 -8
  24. package/dist/dts/OpenAiEmbeddingModel.d.ts.map +1 -1
  25. package/dist/dts/OpenAiLanguageModel.d.ts +123 -43
  26. package/dist/dts/OpenAiLanguageModel.d.ts.map +1 -1
  27. package/dist/dts/OpenAiTelemetry.d.ts +4 -4
  28. package/dist/dts/OpenAiTelemetry.d.ts.map +1 -1
  29. package/dist/dts/OpenAiTokenizer.d.ts +1 -1
  30. package/dist/dts/OpenAiTokenizer.d.ts.map +1 -1
  31. package/dist/dts/OpenAiTool.d.ts +176 -0
  32. package/dist/dts/OpenAiTool.d.ts.map +1 -0
  33. package/dist/dts/index.d.ts +4 -0
  34. package/dist/dts/index.d.ts.map +1 -1
  35. package/dist/esm/Generated.js +5846 -12846
  36. package/dist/esm/Generated.js.map +1 -1
  37. package/dist/esm/OpenAiClient.js +1440 -128
  38. package/dist/esm/OpenAiClient.js.map +1 -1
  39. package/dist/esm/OpenAiEmbeddingModel.js +60 -49
  40. package/dist/esm/OpenAiEmbeddingModel.js.map +1 -1
  41. package/dist/esm/OpenAiLanguageModel.js +949 -325
  42. package/dist/esm/OpenAiLanguageModel.js.map +1 -1
  43. package/dist/esm/OpenAiTelemetry.js +4 -4
  44. package/dist/esm/OpenAiTelemetry.js.map +1 -1
  45. package/dist/esm/OpenAiTokenizer.js +46 -14
  46. package/dist/esm/OpenAiTokenizer.js.map +1 -1
  47. package/dist/esm/OpenAiTool.js +84 -0
  48. package/dist/esm/OpenAiTool.js.map +1 -0
  49. package/dist/esm/index.js +4 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/internal/utilities.js +10 -2
  52. package/dist/esm/internal/utilities.js.map +1 -1
  53. package/package.json +12 -4
  54. package/src/Generated.ts +9692 -5599
  55. package/src/OpenAiClient.ts +1761 -224
  56. package/src/OpenAiEmbeddingModel.ts +70 -62
  57. package/src/OpenAiLanguageModel.ts +1134 -369
  58. package/src/OpenAiTelemetry.ts +9 -9
  59. package/src/OpenAiTokenizer.ts +38 -39
  60. package/src/OpenAiTool.ts +110 -0
  61. package/src/index.ts +5 -0
  62. package/src/internal/utilities.ts +16 -4
@@ -1,37 +1,37 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import { AiError } from "@effect/ai/AiError"
5
- import type * as AiInput from "@effect/ai/AiInput"
6
- import * as AiLanguageModel from "@effect/ai/AiLanguageModel"
7
- import * as AiModel from "@effect/ai/AiModel"
8
- import * as AiResponse from "@effect/ai/AiResponse"
4
+ import * as AiError from "@effect/ai/AiError"
5
+ import * as IdGenerator from "@effect/ai/IdGenerator"
6
+ import * as LanguageModel from "@effect/ai/LanguageModel"
7
+ import * as AiModel from "@effect/ai/Model"
8
+ import type * as Prompt from "@effect/ai/Prompt"
9
+ import type * as Response from "@effect/ai/Response"
9
10
  import type * as Tokenizer from "@effect/ai/Tokenizer"
10
- import * as Arr from "effect/Array"
11
+ import * as Tool from "@effect/ai/Tool"
11
12
  import * as Context from "effect/Context"
13
+ import * as DateTime from "effect/DateTime"
12
14
  import * as Effect from "effect/Effect"
13
15
  import * as Encoding from "effect/Encoding"
14
16
  import { dual } from "effect/Function"
15
17
  import * as Layer from "effect/Layer"
16
- import * as Option from "effect/Option"
17
18
  import * as Predicate from "effect/Predicate"
18
19
  import * as Stream from "effect/Stream"
19
20
  import type { Span } from "effect/Tracer"
20
- import type { Simplify } from "effect/Types"
21
+ import type { DeepMutable, Mutable, Simplify } from "effect/Types"
21
22
  import type * as Generated from "./Generated.js"
22
- import { resolveFinishReason } from "./internal/utilities.js"
23
23
  import * as InternalUtilities from "./internal/utilities.js"
24
+ import type { ResponseStreamEvent } from "./OpenAiClient.js"
24
25
  import { OpenAiClient } from "./OpenAiClient.js"
25
26
  import { addGenAIAnnotations } from "./OpenAiTelemetry.js"
26
27
  import * as OpenAiTokenizer from "./OpenAiTokenizer.js"
27
-
28
- const constDisableValidation = { disableValidation: true } as const
28
+ import * as OpenAiTool from "./OpenAiTool.js"
29
29
 
30
30
  /**
31
31
  * @since 1.0.0
32
32
  * @category Models
33
33
  */
34
- export type Model = typeof Generated.ModelIdsSharedEnum.Encoded
34
+ export type Model = typeof Generated.ChatModel.Encoded | typeof Generated.ModelIdsResponsesEnum.Encoded
35
35
 
36
36
  // =============================================================================
37
37
  // Configuration
@@ -66,18 +66,85 @@ export declare namespace Config {
66
66
  Simplify<
67
67
  Partial<
68
68
  Omit<
69
- typeof Generated.CreateChatCompletionRequest.Encoded,
70
- "messages" | "tools" | "tool_choice" | "stream" | "stream_options" | "functions"
69
+ typeof Generated.CreateResponse.Encoded,
70
+ "input" | "tools" | "tool_choice" | "stream" | "text"
71
71
  >
72
72
  >
73
73
  >
74
- {}
74
+ {
75
+ /**
76
+ * File ID prefixes used to identify file IDs in Responses API.
77
+ * When undefined, all file data is treated as base64 content.
78
+ *
79
+ * Examples:
80
+ * - OpenAI: ['file-'] for IDs like 'file-abc123'
81
+ * - Azure OpenAI: ['assistant-'] for IDs like 'assistant-abc123'
82
+ */
83
+ readonly fileIdPrefixes?: ReadonlyArray<string>
84
+ /**
85
+ * Configuration options for a text response from the model.
86
+ */
87
+ readonly text?: {
88
+ /**
89
+ * Constrains the verbosity of the model's response. Lower values will
90
+ * result in more concise responses, while higher values will result in
91
+ * more verbose responses.
92
+ *
93
+ * Defaults to `"medium"`.
94
+ */
95
+ readonly verbosity?: "low" | "medium" | "high"
96
+ }
97
+ }
75
98
  }
76
99
 
77
100
  // =============================================================================
78
- // Anthropic Provider Metadata
101
+ // OpenAI Provider Options / Metadata
79
102
  // =============================================================================
80
103
 
104
+ declare module "@effect/ai/Prompt" {
105
+ export interface FilePartOptions extends ProviderOptions {
106
+ readonly openai?: {
107
+ /**
108
+ * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`.
109
+ */
110
+ readonly imageDetail?: typeof Generated.InputImageContentDetail.Encoded | undefined
111
+ } | undefined
112
+ }
113
+
114
+ export interface ReasoningPartOptions extends ProviderOptions {
115
+ readonly openai?: {
116
+ /**
117
+ * The ID of the item to reference.
118
+ */
119
+ readonly itemId?: string | undefined
120
+ /**
121
+ * The encrypted content of the reasoning item - populated when a response
122
+ * is generated with `reasoning.encrypted_content` in the `include`
123
+ * parameter.
124
+ */
125
+ readonly encryptedContent?: string | undefined
126
+ } | undefined
127
+ }
128
+
129
+ export interface ToolCallPartOptions extends ProviderOptions {
130
+ readonly openai?: {
131
+ /**
132
+ * The ID of the item to reference.
133
+ */
134
+ readonly itemId?: string | undefined
135
+ } | undefined
136
+ }
137
+
138
+ export interface TextPartOptions extends ProviderOptions {
139
+ readonly openai?: {
140
+ /**
141
+ * The ID of the item to reference.
142
+ */
143
+ readonly itemId?: string | undefined
144
+ } | undefined
145
+ }
146
+ }
147
+
81
148
  /**
82
149
  * @since 1.0.0
83
150
  * @category Context
@@ -96,63 +163,90 @@ export declare namespace ProviderMetadata {
96
163
  * @category Provider Metadata
97
164
  */
98
165
  export interface Service {
99
- /**
100
- * Specifies the latency tier that was used for processing the request.
101
- */
102
- readonly serviceTier?: string
103
- /**
104
- * This fingerprint represents the backend configuration that the model
105
- * executes with.
106
- *
107
- * Can be used in conjunction with the seed request parameter to understand
108
- * when backend changes have been made that might impact determinism.
109
- */
110
- readonly systemFingerprint: string
111
- /**
112
- * When using predicted outputs, the number of tokens in the prediction
113
- * that appeared in the completion.
114
- */
115
- readonly acceptedPredictionTokens: number
116
- /**
117
- * When using predicted outputs, the number of tokens in the prediction
118
- * that did not appear in the completion. However, like reasoning tokens,
119
- * these tokens are still counted in the total completion tokens for
120
- * purposes of billing, output, and context window limits.
121
- */
122
- readonly rejectedPredictionTokens: number
123
- /**
124
- * Audio tokens present in the prompt.
125
- */
126
- readonly inputAudioTokens: number
127
- /**
128
- * Audio tokens generated by the model.
129
- */
130
- readonly outputAudioTokens: number
166
+ "finish": {
167
+ readonly serviceTier?: "default" | "auto" | "flex" | "scale" | "priority" | undefined
168
+ }
169
+
170
+ "reasoning": {
171
+ readonly itemId?: string | undefined
172
+ readonly encryptedContent?: string | undefined
173
+ }
174
+
175
+ "reasoning-start": {
176
+ readonly itemId?: string | undefined
177
+ readonly encryptedContent?: string | undefined
178
+ }
179
+
180
+ "reasoning-delta": {
181
+ readonly itemId?: string | undefined
182
+ }
183
+
184
+ "reasoning-end": {
185
+ readonly itemId?: string | undefined
186
+ readonly encryptedContent?: string | undefined
187
+ }
188
+
189
+ "source": {
190
+ readonly type: "file_citation"
191
+ /**
192
+ * The index of the file in the list of files.
193
+ */
194
+ readonly index: number
195
+ } | {
196
+ readonly type: "url_citation"
197
+ /**
198
+ * The index of the first character of the URL citation in the message.
199
+ */
200
+ readonly startIndex: number
201
+ /**
202
+ * The index of the last character of the URL citation in the message.
203
+ */
204
+ readonly endIndex: number
205
+ }
206
+
207
+ "text": {
208
+ readonly itemId?: string | undefined
209
+ /**
210
+ * If the model emits a refusal content part, the refusal explanation
211
+ * from the model will be contained in the metadata of an empty text
212
+ * part.
213
+ */
214
+ readonly refusal?: string | undefined
215
+ }
216
+
217
+ "text-start": {
218
+ readonly itemId?: string | undefined
219
+ }
220
+
221
+ "tool-call": {
222
+ readonly itemId?: string | undefined
223
+ }
131
224
  }
132
225
  }
133
226
 
134
227
  // =============================================================================
135
- // OpenAi Language Model
228
+ // OpenAI Language Model
136
229
  // =============================================================================
137
230
 
138
231
  /**
139
232
  * @since 1.0.0
140
- * @category AiModel
233
+ * @category Ai Models
141
234
  */
142
235
  export const model = (
143
236
  model: (string & {}) | Model,
144
237
  config?: Omit<Config.Service, "model">
145
- ): AiModel.AiModel<AiLanguageModel.AiLanguageModel, OpenAiClient> => AiModel.make(layer({ model, config }))
238
+ ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> =>
239
+ AiModel.make("openai", layer({ model, config }))
146
240
 
147
241
  /**
148
242
  * @since 1.0.0
149
- * @category AiModel
243
+ * @category Ai Models
150
244
  */
151
245
  export const modelWithTokenizer = (
152
246
  model: (string & {}) | Model,
153
247
  config?: Omit<Config.Service, "model">
154
- ): AiModel.AiModel<AiLanguageModel.AiLanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
155
- AiModel.make(layerWithTokenizer({ model, config }))
248
+ ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
249
+ AiModel.make("openai", layerWithTokenizer({ model, config }))
156
250
 
157
251
  /**
158
252
  * @since 1.0.0
@@ -164,92 +258,56 @@ export const make = Effect.fnUntraced(function*(options: {
164
258
  }) {
165
259
  const client = yield* OpenAiClient
166
260
 
167
- const makeRequest = Effect.fnUntraced(
168
- function*(method: string, { prompt, system, toolChoice, tools }: AiLanguageModel.AiLanguageModelOptions) {
261
+ const makeRequest: (providerOptions: LanguageModel.ProviderOptions) => Effect.Effect<
262
+ typeof Generated.CreateResponse.Encoded,
263
+ AiError.AiError
264
+ > = Effect.fnUntraced(
265
+ function*(providerOptions) {
169
266
  const context = yield* Effect.context<never>()
170
- const useStructured = tools.length === 1 && tools[0].structured
171
- let tool_choice: typeof Generated.ChatCompletionToolChoiceOption.Encoded | undefined = undefined
172
- if (Predicate.isNotUndefined(toolChoice) && !useStructured && tools.length > 0) {
173
- if (toolChoice === "auto" || toolChoice === "required") {
174
- tool_choice = toolChoice
175
- } else if (typeof toolChoice === "object") {
176
- tool_choice = { type: "function", function: { name: toolChoice.tool } }
177
- }
267
+ const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) }
268
+ const messages = yield* prepareMessages(providerOptions, config)
269
+ const { toolChoice, tools } = yield* prepareTools(providerOptions)
270
+ const include = prepareInclude(providerOptions, config)
271
+ const responseFormat = prepareResponseFormat(providerOptions)
272
+ const verbosity = config.text?.verbosity
273
+ const request: typeof Generated.CreateResponse.Encoded = {
274
+ ...config,
275
+ input: messages,
276
+ include,
277
+ text: { format: responseFormat, verbosity },
278
+ tools,
279
+ tool_choice: toolChoice
178
280
  }
179
- const messages = yield* makeMessages(method, system, prompt)
180
- return {
181
- model: options.model,
182
- ...options.config,
183
- ...context.unsafeMap.get(Config.key),
184
- messages,
185
- response_format: useStructured ?
186
- {
187
- type: "json_schema",
188
- json_schema: {
189
- strict: true,
190
- name: tools[0].name,
191
- description: tools[0].description,
192
- schema: tools[0].parameters as any
193
- }
194
- } :
195
- undefined,
196
- tools: !useStructured && tools.length > 0 ?
197
- tools.map((tool) => ({
198
- type: "function",
199
- function: {
200
- name: tool.name,
201
- description: tool.description,
202
- parameters: tool.parameters as any,
203
- strict: true
204
- }
205
- })) :
206
- undefined,
207
- tool_choice
208
- } satisfies typeof Generated.CreateChatCompletionRequest.Encoded
281
+ return request
209
282
  }
210
283
  )
211
284
 
212
- return yield* AiLanguageModel.make({
285
+ return yield* LanguageModel.make({
213
286
  generateText: Effect.fnUntraced(
214
287
  function*(options) {
215
- const structuredTool = options.tools.length === 1 && options.tools[0].structured
216
- ? options.tools[0]
217
- : undefined
218
- const request = yield* makeRequest("generateText", options)
288
+ const request = yield* makeRequest(options)
219
289
  annotateRequest(options.span, request)
220
- const rawResponse = yield* client.client.createChatCompletion(request)
221
- annotateChatResponse(options.span, rawResponse)
222
- const response = yield* makeResponse(rawResponse, "generateText", structuredTool)
223
- return response
224
- },
225
- Effect.catchAll((cause) =>
226
- AiError.is(cause) ? cause : new AiError({
227
- module: "OpenAiLanguageModel",
228
- method: "generateText",
229
- description: "An error occurred",
230
- cause
231
- })
232
- )
290
+ const rawResponse = yield* client.createResponse(request)
291
+ annotateResponse(options.span, rawResponse)
292
+ return yield* makeResponse(rawResponse, options)
293
+ }
233
294
  ),
234
- streamText(options) {
235
- return makeRequest("streamText", options).pipe(
236
- Effect.tap((request) => annotateRequest(options.span, request)),
237
- Effect.map(client.stream),
238
- Stream.unwrap,
239
- Stream.map((response) => {
240
- annotateStreamResponse(options.span, response)
241
- return response
242
- }),
243
- Stream.catchAll((cause) =>
244
- AiError.is(cause) ? cause : new AiError({
245
- module: "OpenAiLanguageModel",
246
- method: "streamText",
247
- description: "An error occurred",
248
- cause
295
+ streamText: Effect.fnUntraced(
296
+ function*(options) {
297
+ const request = yield* makeRequest(options)
298
+ annotateRequest(options.span, request)
299
+ return client.createResponseStream(request)
300
+ },
301
+ (effect, options) =>
302
+ effect.pipe(
303
+ Effect.flatMap((stream) => makeStreamResponse(stream, options)),
304
+ Stream.unwrap,
305
+ Stream.map((response) => {
306
+ annotateStreamResponse(options.span, response)
307
+ return response
249
308
  })
250
309
  )
251
- )
252
- }
310
+ )
253
311
  })
254
312
  })
255
313
 
@@ -260,8 +318,8 @@ export const make = Effect.fnUntraced(function*(options: {
260
318
  export const layer = (options: {
261
319
  readonly model: (string & {}) | Model
262
320
  readonly config?: Omit<Config.Service, "model">
263
- }): Layer.Layer<AiLanguageModel.AiLanguageModel, never, OpenAiClient> =>
264
- Layer.effect(AiLanguageModel.AiLanguageModel, make({ model: options.model, config: options.config }))
321
+ }): Layer.Layer<LanguageModel.LanguageModel, never, OpenAiClient> =>
322
+ Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config }))
265
323
 
266
324
  /**
267
325
  * @since 1.0.0
@@ -270,7 +328,7 @@ export const layer = (options: {
270
328
  export const layerWithTokenizer = (options: {
271
329
  readonly model: (string & {}) | Model
272
330
  readonly config?: Omit<Config.Service, "model">
273
- }): Layer.Layer<AiLanguageModel.AiLanguageModel | Tokenizer.Tokenizer, never, OpenAiClient> =>
331
+ }): Layer.Layer<LanguageModel.LanguageModel | Tokenizer.Tokenizer, never, OpenAiClient> =>
274
332
  Layer.merge(layer(options), OpenAiTokenizer.layer(options))
275
333
 
276
334
  /**
@@ -305,247 +363,805 @@ export const withConfigOverride: {
305
363
  (config) => Effect.provideService(self, Config, { ...config, ...overrides })
306
364
  ))
307
365
 
308
- const makeMessages = Effect.fnUntraced(function*(
309
- method: string,
310
- system: Option.Option<string>,
311
- prompt: AiInput.AiInput
312
- ) {
313
- type Messages = Array<typeof Generated.ChatCompletionRequestMessage.Encoded>
314
- type UserPart = typeof Generated.ChatCompletionRequestUserMessageContentPart.Encoded
315
- const messages: Messages = Option.match(system, {
316
- onNone: () => [],
317
- onSome: (content) => [{ role: "system", content }]
318
- })
319
- for (const message of prompt.messages) {
320
- switch (message._tag) {
321
- case "AssistantMessage": {
322
- let text = ""
323
- const toolCalls: Array<typeof Generated.ChatCompletionMessageToolCall.Encoded> = []
324
- for (const part of message.parts) {
325
- switch (part._tag) {
326
- case "TextPart": {
327
- text += part.text
366
+ // =============================================================================
367
+ // Prompt Conversion
368
+ // =============================================================================
369
+
370
+ const getSystemMessageMode = (model: string): "system" | "developer" =>
371
+ model.startsWith("o") ||
372
+ model.startsWith("gpt-5") ||
373
+ model.startsWith("codex-") ||
374
+ model.startsWith("computer-use")
375
+ ? "developer"
376
+ : "system"
377
+
378
+ const prepareMessages: (
379
+ options: LanguageModel.ProviderOptions,
380
+ config: Config.Service
381
+ ) => Effect.Effect<
382
+ ReadonlyArray<typeof Generated.InputItem.Encoded>,
383
+ AiError.AiError
384
+ > = Effect.fnUntraced(function*(options, config) {
385
+ const messages: Array<typeof Generated.InputItem.Encoded> = []
386
+
387
+ for (const message of options.prompt.content) {
388
+ switch (message.role) {
389
+ case "system": {
390
+ messages.push({
391
+ role: getSystemMessageMode(config.model!),
392
+ content: message.content
393
+ })
394
+ break
395
+ }
396
+
397
+ case "user": {
398
+ const content: Array<typeof Generated.InputContent.Encoded> = []
399
+
400
+ for (let index = 0; index < message.content.length; index++) {
401
+ const part = message.content[index]
402
+
403
+ switch (part.type) {
404
+ case "text": {
405
+ content.push({ type: "input_text", text: part.text })
328
406
  break
329
407
  }
330
- case "ToolCallPart": {
331
- toolCalls.push({
332
- id: part.id,
333
- type: "function",
334
- function: {
335
- name: part.name,
336
- arguments: JSON.stringify(part.params)
408
+
409
+ case "file": {
410
+ if (part.mediaType.startsWith("image/")) {
411
+ const detail = getImageDetail(part)
412
+ const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType
413
+
414
+ if (typeof part.data === "string" && isFileId(part.data, config)) {
415
+ content.push({ type: "input_image", file_id: part.data, detail })
416
+ }
417
+
418
+ if (part.data instanceof URL) {
419
+ content.push({ type: "input_image", image_url: part.data.toString(), detail })
420
+ }
421
+
422
+ if (part.data instanceof Uint8Array) {
423
+ const base64 = Encoding.encodeBase64(part.data)
424
+ const imageUrl = `data:${mediaType};base64,${base64}`
425
+ content.push({ type: "input_image", image_url: imageUrl, detail })
426
+ }
427
+ }
428
+
429
+ if (part.mediaType === "application/pdf") {
430
+ if (typeof part.data === "string" && isFileId(part.data, config)) {
431
+ content.push({ type: "input_file", file_id: part.data })
432
+ }
433
+
434
+ if (part.data instanceof URL) {
435
+ content.push({ type: "input_file", file_url: part.data.toString() })
436
+ }
437
+
438
+ if (part.data instanceof Uint8Array) {
439
+ const base64 = Encoding.encodeBase64(part.data)
440
+ const fileName = part.fileName ?? `part-${index}.pdf`
441
+ const fileData = `data:application/pdf;base64,${base64}`
442
+ content.push({ type: "input_file", filename: fileName, file_data: fileData })
337
443
  }
444
+ }
445
+
446
+ return yield* new AiError.MalformedInput({
447
+ module: "OpenAiLanguageModel",
448
+ method: "prepareMessages",
449
+ description: `Detected unsupported media type for file: '${part.mediaType}'`
450
+ })
451
+ }
452
+ }
453
+ }
454
+
455
+ messages.push({ role: "user", content })
456
+
457
+ break
458
+ }
459
+
460
+ case "assistant": {
461
+ const reasoningMessages: Record<string, DeepMutable<typeof Generated.ReasoningItem.Encoded>> = {}
462
+
463
+ for (const part of message.content) {
464
+ switch (part.type) {
465
+ case "text": {
466
+ messages.push({
467
+ role: "assistant",
468
+ content: [{ type: "output_text", text: part.text }],
469
+ id: getItemId(part)
338
470
  })
339
471
  break
340
472
  }
473
+
474
+ case "reasoning": {
475
+ const options = part.options.openai
476
+
477
+ if (Predicate.isNotUndefined(options?.itemId)) {
478
+ const reasoningMessage = reasoningMessages[options.itemId]
479
+ const summaryParts: Mutable<typeof Generated.ReasoningItem.fields.summary.Encoded> = []
480
+
481
+ if (part.text.length > 0) {
482
+ summaryParts.push({ type: "summary_text", text: part.text })
483
+ }
484
+
485
+ if (Predicate.isUndefined(reasoningMessage)) {
486
+ reasoningMessages[options.itemId] = {
487
+ id: options.itemId,
488
+ type: "reasoning",
489
+ summary: summaryParts,
490
+ encrypted_content: options.encryptedContent
491
+ }
492
+ messages.push(reasoningMessages[options.itemId])
493
+ } else {
494
+ for (const summaryPart of summaryParts) {
495
+ reasoningMessage.summary.push(summaryPart)
496
+ }
497
+ }
498
+ }
499
+
500
+ break
501
+ }
502
+
503
+ case "tool-call": {
504
+ if (!part.providerExecuted) {
505
+ messages.push({
506
+ id: getItemId(part),
507
+ type: "function_call",
508
+ call_id: part.id,
509
+ name: part.name,
510
+ arguments: JSON.stringify(part.params)
511
+ })
512
+ }
513
+
514
+ break
515
+ }
341
516
  }
342
517
  }
343
- messages.push({
344
- role: "assistant",
345
- content: text,
346
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined
347
- })
348
518
 
349
519
  break
350
520
  }
351
- case "ToolMessage": {
352
- for (const part of message.parts) {
521
+
522
+ case "tool": {
523
+ for (const part of message.content) {
353
524
  messages.push({
354
- role: "tool",
355
- tool_call_id: part.id,
356
- content: JSON.stringify(part.result)
525
+ type: "function_call_output",
526
+ call_id: part.id,
527
+ result: JSON.stringify(part.result)
357
528
  })
358
529
  }
530
+
359
531
  break
360
532
  }
361
- case "UserMessage": {
362
- // Handle the case where the message content is just a single piece of text
363
- if (message.parts.length === 1 && message.parts[0]._tag === "TextPart") {
364
- messages.push({ role: "user", content: message.parts[0].text })
533
+ }
534
+ }
535
+
536
+ return messages
537
+ })
538
+
539
+ // =============================================================================
540
+ // Response Conversion
541
+ // =============================================================================
542
+
543
+ const makeResponse: (
544
+ response: Generated.Response,
545
+ options: LanguageModel.ProviderOptions
546
+ ) => Effect.Effect<
547
+ Array<Response.PartEncoded>,
548
+ never,
549
+ IdGenerator.IdGenerator
550
+ > = Effect.fnUntraced(
551
+ function*(response, options) {
552
+ const idGenerator = yield* IdGenerator.IdGenerator
553
+
554
+ const webSearchTool = options.tools.find((tool) =>
555
+ Tool.isProviderDefined(tool) &&
556
+ (tool.id === "openai.web_search" ||
557
+ tool.id === "openai.web_search_preview")
558
+ ) as Tool.AnyProviderDefined | undefined
559
+
560
+ let hasToolCalls = false
561
+ const parts: Array<Response.PartEncoded> = []
562
+
563
+ const createdAt = new Date(response.created_at * 1000)
564
+ parts.push({
565
+ type: "response-metadata",
566
+ id: response.id,
567
+ modelId: response.model,
568
+ timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt))
569
+ })
570
+
571
+ for (const part of response.output) {
572
+ switch (part.type) {
573
+ case "message": {
574
+ for (const contentPart of part.content) {
575
+ switch (contentPart.type) {
576
+ case "output_text": {
577
+ parts.push({
578
+ type: "text",
579
+ text: contentPart.text,
580
+ metadata: { [ProviderMetadata.key]: { itemId: part.id } }
581
+ })
582
+
583
+ for (const annotation of contentPart.annotations) {
584
+ if (annotation.type === "file_citation") {
585
+ const metadata = {
586
+ type: annotation.type,
587
+ index: annotation.index
588
+ }
589
+
590
+ parts.push({
591
+ type: "source",
592
+ sourceType: "document",
593
+ id: yield* idGenerator.generateId(),
594
+ mediaType: "text/plain",
595
+ title: annotation.filename ?? "Untitled Document",
596
+ metadata: { [ProviderMetadata.key]: metadata }
597
+ })
598
+ }
599
+
600
+ if (annotation.type === "url_citation") {
601
+ const metadata = {
602
+ type: annotation.type,
603
+ startIndex: annotation.start_index,
604
+ endIndex: annotation.end_index
605
+ }
606
+
607
+ parts.push({
608
+ type: "source",
609
+ sourceType: "url",
610
+ id: yield* idGenerator.generateId(),
611
+ url: annotation.url,
612
+ title: annotation.title,
613
+ metadata: { [ProviderMetadata.key]: metadata }
614
+ })
615
+ }
616
+ }
617
+
618
+ break
619
+ }
620
+ case "refusal": {
621
+ parts.push({
622
+ type: "text",
623
+ text: "",
624
+ metadata: { [ProviderMetadata.key]: { refusal: contentPart.refusal } }
625
+ })
626
+
627
+ break
628
+ }
629
+ }
630
+ }
631
+
365
632
  break
366
633
  }
367
- const content: Array<UserPart> = []
368
- for (let index = 0; index < message.parts.length; index++) {
369
- const part = message.parts[index]
370
- switch (part._tag) {
371
- // TODO: review file inputs
372
- case "FilePart": {
373
- const data = Encoding.encodeBase64(part.data)
374
- switch (part.mediaType) {
375
- case "audio/wav": {
376
- content.push({
377
- type: "input_audio",
378
- input_audio: { data, format: "wav" }
379
- })
380
- break
634
+
635
+ case "function_call": {
636
+ hasToolCalls = true
637
+
638
+ parts.push({
639
+ type: "tool-call",
640
+ id: part.call_id,
641
+ name: part.name,
642
+ params: JSON.parse(part.arguments),
643
+ metadata: { [ProviderMetadata.key]: { itemId: part.id } }
644
+ })
645
+
646
+ break
647
+ }
648
+
649
+ case "code_interpreter_call": {
650
+ parts.push({
651
+ type: "tool-call",
652
+ id: part.id,
653
+ name: "OpenAiCodeInterpreter",
654
+ params: { code: part.code, container_id: part.container_id },
655
+ providerName: "code_interpreter",
656
+ providerExecuted: true
657
+ })
658
+
659
+ parts.push({
660
+ type: "tool-result",
661
+ id: part.id,
662
+ name: "OpenAiCodeInterpreter",
663
+ result: { outputs: part.outputs },
664
+ providerName: "code_interpreter",
665
+ providerExecuted: true
666
+ })
667
+
668
+ break
669
+ }
670
+
671
+ case "file_search_call": {
672
+ parts.push({
673
+ type: "tool-call",
674
+ id: part.id,
675
+ name: "OpenAiFileSearch",
676
+ params: {},
677
+ providerName: "file_search",
678
+ providerExecuted: true
679
+ })
680
+
681
+ parts.push({
682
+ type: "tool-result",
683
+ id: part.id,
684
+ name: "OpenAiFileSearch",
685
+ result: {
686
+ status: part.status,
687
+ queries: part.queries,
688
+ ...(part.results && { results: part.results })
689
+ },
690
+ providerName: "file_search",
691
+ providerExecuted: true
692
+ })
693
+
694
+ break
695
+ }
696
+
697
+ case "web_search_call": {
698
+ parts.push({
699
+ type: "tool-call",
700
+ id: part.id,
701
+ name: webSearchTool?.name ?? "OpenAiWebSearch",
702
+ params: { action: part.action },
703
+ providerName: webSearchTool?.providerName ?? "web_search",
704
+ providerExecuted: true
705
+ })
706
+
707
+ parts.push({
708
+ type: "tool-result",
709
+ id: part.id,
710
+ name: webSearchTool?.name ?? "OpenAiWebSearch",
711
+ result: { status: part.status },
712
+ providerName: webSearchTool?.providerName ?? "web_search",
713
+ providerExecuted: true
714
+ })
715
+
716
+ break
717
+ }
718
+
719
+ // TODO(Max): support computer use
720
+ // case "computer_call": {
721
+ // parts.push({
722
+ // type: "tool-call",
723
+ // id: part.id,
724
+ // name: "OpenAiComputerUse",
725
+ // params: { action: part.action },
726
+ // providerName: webSearchTool?.providerName ?? "web_search",
727
+ // providerExecuted: true
728
+ // })
729
+ //
730
+ // parts.push({
731
+ // type: "tool-result",
732
+ // id: part.id,
733
+ // name: webSearchTool?.name ?? "OpenAiWebSearch",
734
+ // result: { status: part.status },
735
+ // providerName: webSearchTool?.providerName ?? "web_search",
736
+ // providerExecuted: true
737
+ // })
738
+ // break
739
+ // }
740
+
741
+ case "reasoning": {
742
+ // If there are no summary parts, we have to add an empty one to
743
+ // propagate the part identifier
744
+ if (part.summary.length === 0) {
745
+ parts.push({
746
+ type: "reasoning",
747
+ text: "",
748
+ metadata: { [ProviderMetadata.key]: { itemId: part.id } }
749
+ })
750
+ } else {
751
+ for (const summary of part.summary) {
752
+ const metadata = {
753
+ itemId: part.id,
754
+ encryptedContent: part.encrypted_content ?? undefined
755
+ }
756
+ parts.push({
757
+ type: "reasoning",
758
+ text: summary.text,
759
+ metadata: { [ProviderMetadata.key]: metadata }
760
+ })
761
+ }
762
+ }
763
+
764
+ break
765
+ }
766
+ }
767
+ }
768
+
769
+ const finishReason = InternalUtilities.resolveFinishReason(
770
+ response.incomplete_details?.reason,
771
+ hasToolCalls
772
+ )
773
+
774
+ const metadata = {
775
+ serviceTier: response.service_tier
776
+ }
777
+
778
+ parts.push({
779
+ type: "finish",
780
+ reason: finishReason,
781
+ usage: {
782
+ inputTokens: response.usage?.input_tokens,
783
+ outputTokens: response.usage?.output_tokens,
784
+ totalTokens: (response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0),
785
+ reasoningTokens: response.usage?.output_tokens_details?.reasoning_tokens,
786
+ cachedInputTokens: response.usage?.input_tokens_details?.cached_tokens
787
+ },
788
+ metadata: { [ProviderMetadata.key]: metadata }
789
+ })
790
+
791
+ return parts
792
+ }
793
+ )
794
+
795
+ const makeStreamResponse: (
796
+ stream: Stream.Stream<ResponseStreamEvent, AiError.AiError>,
797
+ options: LanguageModel.ProviderOptions
798
+ ) => Effect.Effect<
799
+ Stream.Stream<Response.StreamPartEncoded, AiError.AiError>,
800
+ never,
801
+ IdGenerator.IdGenerator
802
+ > = Effect.fnUntraced(
803
+ function*(stream, options) {
804
+ const idGenerator = yield* IdGenerator.IdGenerator
805
+
806
+ let hasToolCalls = false
807
+
808
+ const activeReasoning: Record<string, {
809
+ readonly summaryParts: Array<number>
810
+ readonly encryptedContent: string | undefined
811
+ }> = {}
812
+
813
+ const activeToolCalls: Record<number, {
814
+ readonly id: string
815
+ readonly name: string
816
+ }> = {}
817
+
818
+ const webSearchTool = options.tools.find((tool) =>
819
+ Tool.isProviderDefined(tool) &&
820
+ (tool.id === "openai.web_search" ||
821
+ tool.id === "openai.web_search_preview")
822
+ ) as Tool.AnyProviderDefined | undefined
823
+
824
+ return stream.pipe(
825
+ Stream.mapEffect(Effect.fnUntraced(function*(event) {
826
+ const parts: Array<Response.StreamPartEncoded> = []
827
+
828
+ switch (event.type) {
829
+ case "response.created": {
830
+ const createdAt = new Date(event.response.created_at * 1000)
831
+ parts.push({
832
+ type: "response-metadata",
833
+ id: event.response.id,
834
+ modelId: event.response.model,
835
+ timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt))
836
+ })
837
+ break
838
+ }
839
+
840
+ case "error": {
841
+ parts.push({ type: "error", error: event })
842
+ break
843
+ }
844
+
845
+ case "response.completed":
846
+ case "response.incomplete":
847
+ case "response.failed": {
848
+ parts.push({
849
+ type: "finish",
850
+ reason: InternalUtilities.resolveFinishReason(
851
+ event.response.incomplete_details?.reason,
852
+ hasToolCalls
853
+ ),
854
+ usage: {
855
+ inputTokens: event.response.usage?.input_tokens,
856
+ outputTokens: event.response.usage?.output_tokens,
857
+ totalTokens: (event.response.usage?.input_tokens ?? 0) + (event.response.usage?.output_tokens ?? 0),
858
+ reasoningTokens: event.response.usage?.output_tokens_details?.reasoning_tokens,
859
+ cachedInputTokens: event.response.usage?.input_tokens_details?.cached_tokens
860
+ },
861
+ metadata: { [ProviderMetadata.key]: { serviceTier: event.response.service_tier } }
862
+ })
863
+ break
864
+ }
865
+
866
+ case "response.output_item.added": {
867
+ switch (event.item.type) {
868
+ case "computer_call": {
869
+ // TODO(Max): support computer use
870
+ break
871
+ }
872
+
873
+ case "file_search_call": {
874
+ activeToolCalls[event.output_index] = {
875
+ id: event.item.id,
876
+ name: "OpenAiFileSearch"
381
877
  }
382
- case "audio/mp3":
383
- case "audio/mpeg": {
384
- content.push({
385
- type: "input_audio",
386
- input_audio: { data, format: "mp3" }
387
- })
388
- break
878
+ parts.push({
879
+ type: "tool-params-start",
880
+ id: event.item.id,
881
+ name: "OpenAiFileSearch",
882
+ providerName: "file_search",
883
+ providerExecuted: true
884
+ })
885
+ break
886
+ }
887
+
888
+ case "function_call": {
889
+ activeToolCalls[event.output_index] = {
890
+ id: event.item.call_id,
891
+ name: event.item.name
389
892
  }
390
- case "application/pdf": {
391
- content.push({
392
- type: "file",
393
- file: {
394
- filename: part.name ?? `part-${index}.pdf`,
395
- file_data: `data:application/pdf;base64,${data}`
893
+ parts.push({
894
+ type: "tool-params-start",
895
+ id: event.item.call_id,
896
+ name: event.item.name
897
+ })
898
+ break
899
+ }
900
+
901
+ case "message": {
902
+ parts.push({
903
+ type: "text-start",
904
+ id: event.item.id,
905
+ metadata: { [ProviderMetadata.key]: { itemId: event.item.id } }
906
+ })
907
+ break
908
+ }
909
+
910
+ case "reasoning": {
911
+ activeReasoning[event.item.id] = {
912
+ summaryParts: [0],
913
+ encryptedContent: event.item.encrypted_content
914
+ }
915
+ parts.push({
916
+ type: "reasoning-start",
917
+ id: `${event.item.id}:0`,
918
+ metadata: {
919
+ [ProviderMetadata.key]: {
920
+ itemId: event.item.id,
921
+ encryptedContent: event.item.encrypted_content
396
922
  }
397
- })
398
- break
923
+ }
924
+ })
925
+ break
926
+ }
927
+
928
+ case "web_search_call": {
929
+ activeToolCalls[event.output_index] = {
930
+ id: event.item.id,
931
+ name: webSearchTool?.name ?? "OpenAiWebSearch"
399
932
  }
400
- default: {
401
- return yield* new AiError({
402
- module: "OpenAiLanguageModel",
403
- method: "",
404
- description: `OpenAi does not support file inputs of type "${part.mediaType}"`
933
+ parts.push({
934
+ type: "tool-params-start",
935
+ id: event.item.id,
936
+ name: webSearchTool?.name ?? "OpenAiWebSearch",
937
+ providerName: webSearchTool?.providerName ?? "web_search",
938
+ providerExecuted: true
939
+ })
940
+ break
941
+ }
942
+ }
943
+
944
+ break
945
+ }
946
+
947
+ case "response.output_item.done": {
948
+ switch (event.item.type) {
949
+ case "code_interpreter_call": {
950
+ parts.push({
951
+ type: "tool-call",
952
+ id: event.item.id,
953
+ name: "OpenAiCodeInterpreter",
954
+ params: { code: event.item.code, container_id: event.item.container_id },
955
+ providerName: "code_interpreter",
956
+ providerExecuted: true
957
+ })
958
+ parts.push({
959
+ type: "tool-result",
960
+ id: event.item.id,
961
+ name: "OpenAiCodeInterpreter",
962
+ result: { outputs: event.item.outputs },
963
+ providerName: "code_interpreter",
964
+ providerExecuted: true
965
+ })
966
+ break
967
+ }
968
+
969
+ // TODO(Max): support computer use
970
+ case "computer_call": {
971
+ break
972
+ }
973
+
974
+ case "file_search_call": {
975
+ delete activeToolCalls[event.output_index]
976
+ parts.push({
977
+ type: "tool-params-end",
978
+ id: event.item.id
979
+ })
980
+ parts.push({
981
+ type: "tool-call",
982
+ id: event.item.id,
983
+ name: "OpenAiFileSearch",
984
+ params: {},
985
+ providerName: "file_search",
986
+ providerExecuted: true
987
+ })
988
+ parts.push({
989
+ type: "tool-result",
990
+ id: event.item.id,
991
+ name: "OpenAiFileSearch",
992
+ result: {
993
+ status: event.item.status,
994
+ queries: event.item.queries,
995
+ ...(event.item.results && { results: event.item.results })
996
+ },
997
+ providerName: "file_search",
998
+ providerExecuted: true
999
+ })
1000
+ break
1001
+ }
1002
+
1003
+ case "function_call": {
1004
+ hasToolCalls = true
1005
+ delete activeToolCalls[event.output_index]
1006
+ parts.push({
1007
+ type: "tool-params-end",
1008
+ id: event.item.call_id
1009
+ })
1010
+ parts.push({
1011
+ type: "tool-call",
1012
+ id: event.item.call_id,
1013
+ name: event.item.name,
1014
+ params: JSON.parse(event.item.arguments),
1015
+ metadata: { [ProviderMetadata.key]: { itemId: event.item.id } }
1016
+ })
1017
+ break
1018
+ }
1019
+
1020
+ case "message": {
1021
+ parts.push({
1022
+ type: "text-end",
1023
+ id: event.item.id
1024
+ })
1025
+ break
1026
+ }
1027
+
1028
+ case "reasoning": {
1029
+ const reasoningPart = activeReasoning[event.item.id]
1030
+ for (const summaryIndex of reasoningPart.summaryParts) {
1031
+ parts.push({
1032
+ type: "reasoning-end",
1033
+ id: `${event.item.id}:${summaryIndex}`,
1034
+ metadata: {
1035
+ [ProviderMetadata.key]: {
1036
+ itemId: event.item.id,
1037
+ encryptedContent: event.item.encrypted_content
1038
+ }
1039
+ }
405
1040
  })
406
1041
  }
1042
+ delete activeReasoning[event.item.id]
1043
+ break
1044
+ }
1045
+
1046
+ case "web_search_call": {
1047
+ delete activeToolCalls[event.output_index]
1048
+ parts.push({
1049
+ type: "tool-params-end",
1050
+ id: event.item.id
1051
+ })
1052
+ parts.push({
1053
+ type: "tool-call",
1054
+ id: event.item.id,
1055
+ name: "OpenAiWebSearch",
1056
+ params: { action: event.item.action },
1057
+ providerName: "web_search",
1058
+ providerExecuted: true
1059
+ })
1060
+ parts.push({
1061
+ type: "tool-result",
1062
+ id: event.item.id,
1063
+ name: "OpenAiWebSearch",
1064
+ result: { status: event.item.status },
1065
+ providerName: "web_search",
1066
+ providerExecuted: true
1067
+ })
1068
+ break
407
1069
  }
408
- break
409
1070
  }
410
- case "FileUrlPart": {
411
- return yield* new AiError({
412
- module: "OpenAiLanguageModel",
413
- method,
414
- description: "OpenAi does not support file content parts with URL data"
1071
+
1072
+ break
1073
+ }
1074
+
1075
+ case "response.output_text.delta": {
1076
+ parts.push({
1077
+ type: "text-delta",
1078
+ id: event.item_id,
1079
+ delta: event.delta
1080
+ })
1081
+ break
1082
+ }
1083
+
1084
+ case "response.output_text.annotation.added": {
1085
+ if (event.annotation.type === "file_citation") {
1086
+ parts.push({
1087
+ type: "source",
1088
+ sourceType: "document",
1089
+ id: yield* idGenerator.generateId(),
1090
+ mediaType: "text/plain",
1091
+ title: event.annotation.filename ?? "Untitled Document",
1092
+ fileName: event.annotation.filename ?? event.annotation.file_id
415
1093
  })
416
1094
  }
417
- case "TextPart": {
418
- content.push({ type: "text", text: part.text })
419
- break
1095
+ if (event.annotation.type === "url_citation") {
1096
+ parts.push({
1097
+ type: "source",
1098
+ sourceType: "url",
1099
+ id: yield* idGenerator.generateId(),
1100
+ url: event.annotation.url,
1101
+ title: event.annotation.title
1102
+ })
420
1103
  }
421
- case "ImagePart": {
422
- const mediaType = part.mediaType ?? "image/jpeg"
423
- const base64 = Encoding.encodeBase64(part.data)
424
- const url = `data:${mediaType};base64,${base64}`
425
- content.push({ type: "image_url", image_url: { url } })
426
- break
1104
+ break
1105
+ }
1106
+
1107
+ case "response.function_call_arguments.delta": {
1108
+ const toolCallPart = activeToolCalls[event.output_index]
1109
+ if (Predicate.isNotUndefined(toolCallPart)) {
1110
+ parts.push({
1111
+ type: "tool-params-delta",
1112
+ id: toolCallPart.id,
1113
+ delta: event.delta
1114
+ })
427
1115
  }
428
- case "ImageUrlPart": {
429
- // TODO: provider options
430
- // const detail = part.providerOptions?.openai?.imageDetail as any
431
- content.push({ type: "image_url", image_url: { url: part.url.toString() } })
1116
+ break
1117
+ }
1118
+
1119
+ case "response.reasoning_summary_part.added": {
1120
+ // The first reasoning start is pushed in the `response.output_item.added` block
1121
+ if (event.summary_index > 0) {
1122
+ const reasoningPart = activeReasoning[event.item_id]
1123
+ if (Predicate.isNotUndefined(reasoningPart)) {
1124
+ reasoningPart.summaryParts.push(event.summary_index)
1125
+ }
1126
+ parts.push({
1127
+ type: "reasoning-start",
1128
+ id: `${event.item_id}:${event.summary_index}`,
1129
+ metadata: {
1130
+ [ProviderMetadata.key]: {
1131
+ itemId: event.item_id,
1132
+ encryptedContent: reasoningPart?.encryptedContent
1133
+ }
1134
+ }
1135
+ })
432
1136
  }
1137
+ break
1138
+ }
1139
+
1140
+ case "response.reasoning_summary_text.delta": {
1141
+ parts.push({
1142
+ type: "reasoning-delta",
1143
+ id: `${event.item_id}:${event.summary_index}`,
1144
+ delta: event.delta,
1145
+ metadata: { [ProviderMetadata.key]: { itemId: event.item_id } }
1146
+ })
1147
+ break
433
1148
  }
434
1149
  }
435
- if (Arr.isNonEmptyArray(content)) {
436
- messages.push({
437
- role: "user",
438
- name: message.userName,
439
- content
440
- })
441
- }
442
- break
443
- }
444
- }
445
- }
446
- if (Arr.isNonEmptyReadonlyArray(messages)) {
447
- return messages
448
- }
449
- return yield* new AiError({
450
- module: "OpenAiLanguageModel",
451
- method,
452
- description: "Prompt contained no messages"
453
- })
454
- })
455
1150
 
456
- const makeResponse = Effect.fnUntraced(function*(
457
- response: typeof Generated.CreateChatCompletionResponse.Type,
458
- method: string,
459
- structuredTool?: AiLanguageModel.AiLanguageModelOptions["tools"][number]
460
- ) {
461
- const choice = response.choices[0]
462
- if (Predicate.isUndefined(choice)) {
463
- return yield* new AiError({
464
- module: "OpenAiLanguageModel",
465
- method,
466
- description: "Could not get response"
467
- })
468
- }
469
- const parts: Array<AiResponse.Part> = []
470
- parts.push(
471
- new AiResponse.MetadataPart({
472
- id: response.id,
473
- model: response.model,
474
- // OpenAi returns the `created` time in seconds
475
- timestamp: new Date(response.created * 1000)
476
- }, constDisableValidation)
477
- )
478
- const finishReason = resolveFinishReason(choice.finish_reason)
479
- const inputTokens = response.usage?.prompt_tokens ?? 0
480
- const outputTokens = response.usage?.completion_tokens ?? 0
481
- const totalTokens = inputTokens + outputTokens
482
- const metadata: Record<string, unknown> = {}
483
- if (Predicate.isNotUndefined(response.service_tier)) {
484
- metadata.serviceTier = response.service_tier
485
- }
486
- if (Predicate.isNotUndefined(response.system_fingerprint)) {
487
- metadata.systemFingerprint = response.system_fingerprint
488
- }
489
- if (Predicate.isNotUndefined(response.usage?.completion_tokens_details?.accepted_prediction_tokens)) {
490
- metadata.acceptedPredictionTokens = response.usage?.completion_tokens_details?.accepted_prediction_tokens ?? 0
491
- }
492
- if (Predicate.isNotUndefined(response.usage?.completion_tokens_details?.rejected_prediction_tokens)) {
493
- metadata.rejectedPredictionTokens = response.usage?.completion_tokens_details?.rejected_prediction_tokens ?? 0
494
- }
495
- if (Predicate.isNotUndefined(response.usage?.prompt_tokens_details?.audio_tokens)) {
496
- metadata.inputAudioTokens = response.usage?.prompt_tokens_details?.audio_tokens ?? 0
497
- }
498
- if (Predicate.isNotUndefined(response.usage?.completion_tokens_details?.audio_tokens)) {
499
- metadata.outputAudioTokens = response.usage?.completion_tokens_details?.audio_tokens ?? 0
500
- }
501
- parts.push(
502
- new AiResponse.FinishPart({
503
- reason: finishReason,
504
- usage: new AiResponse.Usage({
505
- inputTokens,
506
- outputTokens,
507
- totalTokens,
508
- reasoningTokens: response.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
509
- cacheReadInputTokens: response.usage?.prompt_tokens_details?.cached_tokens ?? 0,
510
- cacheWriteInputTokens: 0
511
- }, constDisableValidation),
512
- providerMetadata: { [InternalUtilities.ProviderMetadataKey]: metadata }
513
- }, constDisableValidation)
514
- )
515
- if (Predicate.isNotNullable(choice.message.content)) {
516
- parts.push(
517
- new AiResponse.TextPart({
518
- text: choice.message.content
519
- }, constDisableValidation)
1151
+ return parts
1152
+ })),
1153
+ Stream.flattenIterables
520
1154
  )
521
1155
  }
522
- const output = new AiResponse.AiResponse({ parts }, constDisableValidation)
523
- if (Predicate.isNotUndefined(structuredTool)) {
524
- return yield* AiResponse.withToolCallsJson(output, [{
525
- id: response.id,
526
- name: structuredTool.name,
527
- params: choice.message.content!
528
- }])
529
- }
530
- if (
531
- Predicate.isNotUndefined(choice.message.tool_calls) &&
532
- choice.message.tool_calls.length > 0
533
- ) {
534
- return yield* AiResponse.withToolCallsJson(
535
- output,
536
- choice.message.tool_calls.map((tool) => ({
537
- id: tool.id,
538
- name: tool.function.name,
539
- params: tool.function.arguments
540
- }))
541
- )
542
- }
543
- return output
544
- })
1156
+ )
1157
+
1158
+ // =============================================================================
1159
+ // Telemetry
1160
+ // =============================================================================
545
1161
 
546
1162
  const annotateRequest = (
547
1163
  span: Span,
548
- request: typeof Generated.CreateChatCompletionRequest.Encoded
1164
+ request: typeof Generated.CreateResponse.Encoded
549
1165
  ): void => {
550
1166
  addGenAIAnnotations(span, {
551
1167
  system: "openai",
@@ -554,66 +1170,215 @@ const annotateRequest = (
554
1170
  model: request.model,
555
1171
  temperature: request.temperature,
556
1172
  topP: request.top_p,
557
- maxTokens: request.max_tokens,
558
- stopSequences: Arr.ensure(request.stop).filter(Predicate.isNotNullable),
559
- frequencyPenalty: request.frequency_penalty,
560
- presencePenalty: request.presence_penalty,
561
- seed: request.seed
1173
+ maxTokens: request.max_output_tokens
562
1174
  },
563
1175
  openai: {
564
1176
  request: {
565
- responseFormat: request.response_format?.type,
1177
+ responseFormat: request.text?.format?.type,
566
1178
  serviceTier: request.service_tier
567
1179
  }
568
1180
  }
569
1181
  })
570
1182
  }
571
1183
 
572
- const annotateChatResponse = (
573
- span: Span,
574
- response: typeof Generated.CreateChatCompletionResponse.Type
575
- ): void => {
1184
+ const annotateResponse = (span: Span, response: Generated.Response): void => {
1185
+ const finishReason = response.incomplete_details?.reason
576
1186
  addGenAIAnnotations(span, {
577
1187
  response: {
578
1188
  id: response.id,
579
1189
  model: response.model,
580
- finishReasons: response.choices.map((choice) => choice.finish_reason)
1190
+ finishReasons: Predicate.isNotUndefined(finishReason) ? [finishReason] : undefined
581
1191
  },
582
1192
  usage: {
583
- inputTokens: response.usage?.prompt_tokens,
584
- outputTokens: response.usage?.completion_tokens
1193
+ inputTokens: response.usage?.input_tokens,
1194
+ outputTokens: response.usage?.output_tokens
585
1195
  },
586
1196
  openai: {
587
1197
  response: {
588
- systemFingerprint: response.system_fingerprint,
589
1198
  serviceTier: response.service_tier
590
1199
  }
591
1200
  }
592
1201
  })
593
1202
  }
594
1203
 
595
- const annotateStreamResponse = (
596
- span: Span,
597
- response: AiResponse.AiResponse
598
- ) => {
599
- const metadataPart = response.parts.find((part) => part._tag === "MetadataPart")
600
- const finishPart = response.parts.find((part) => part._tag === "FinishPart")
601
- const providerMetadata = finishPart?.providerMetadata[ProviderMetadata.key]
602
- addGenAIAnnotations(span, {
603
- response: {
604
- id: metadataPart?.id,
605
- model: metadataPart?.model,
606
- finishReasons: finishPart?.reason ? [finishPart.reason] : undefined
607
- },
608
- usage: {
609
- inputTokens: finishPart?.usage.inputTokens,
610
- outputTokens: finishPart?.usage.outputTokens
611
- },
612
- openai: {
1204
+ const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => {
1205
+ if (part.type === "response-metadata") {
1206
+ addGenAIAnnotations(span, {
613
1207
  response: {
614
- serviceTier: providerMetadata?.serviceTier as string | undefined,
615
- systemFingerprint: providerMetadata?.systemFingerprint as string | undefined
1208
+ id: part.id,
1209
+ model: part.modelId
1210
+ }
1211
+ })
1212
+ }
1213
+ if (part.type === "finish") {
1214
+ const serviceTier = part.metadata?.[ProviderMetadata.key]?.serviceTier as string | undefined
1215
+ addGenAIAnnotations(span, {
1216
+ response: {
1217
+ finishReasons: [part.reason]
1218
+ },
1219
+ usage: {
1220
+ inputTokens: part.usage.inputTokens,
1221
+ outputTokens: part.usage.outputTokens
1222
+ },
1223
+ openai: {
1224
+ response: { serviceTier }
1225
+ }
1226
+ })
1227
+ }
1228
+ }
1229
+
1230
+ // =============================================================================
1231
+ // Tool Calling
1232
+ // =============================================================================
1233
+
1234
+ type OpenAiToolChoice = typeof Generated.CreateResponse.fields.tool_choice.from.Encoded
1235
+
1236
+ const prepareTools: (options: LanguageModel.ProviderOptions) => Effect.Effect<{
1237
+ readonly tools: ReadonlyArray<typeof Generated.Tool.Encoded> | undefined
1238
+ readonly toolChoice: OpenAiToolChoice | undefined
1239
+ }, AiError.AiError> = Effect.fnUntraced(function*(options) {
1240
+ // Return immediately if no tools are in the toolkit
1241
+ if (options.tools.length === 0) {
1242
+ return { tools: undefined, toolChoice: undefined }
1243
+ }
1244
+
1245
+ const tools: Array<typeof Generated.Tool.Encoded> = []
1246
+ let toolChoice: OpenAiToolChoice | undefined = undefined
1247
+
1248
+ // Filter the incoming tools down to the set of allowed tools as indicated by
1249
+ // the tool choice. This must be done here given that there is no tool name
1250
+ // in OpenAI's provider-defined tools, so there would be no way to perform
1251
+ // this filter otherwise
1252
+ let allowedTools = options.tools
1253
+ if (typeof options.toolChoice === "object" && "oneOf" in options.toolChoice) {
1254
+ const allowedToolNames = new Set(options.toolChoice.oneOf)
1255
+ allowedTools = options.tools.filter((tool) => allowedToolNames.has(tool.name))
1256
+ toolChoice = options.toolChoice.mode === "required" ? "required" : "auto"
1257
+ }
1258
+
1259
+ // Convert the tools in the toolkit to the provider-defined format
1260
+ for (const tool of allowedTools) {
1261
+ if (Tool.isUserDefined(tool)) {
1262
+ tools.push({
1263
+ type: "function",
1264
+ name: tool.name,
1265
+ description: Tool.getDescription(tool as any),
1266
+ parameters: Tool.getJsonSchema(tool as any) as any,
1267
+ strict: true
1268
+ })
1269
+ }
1270
+
1271
+ if (Tool.isProviderDefined(tool)) {
1272
+ switch (tool.id) {
1273
+ case "openai.code_interpreter": {
1274
+ tools.push({
1275
+ ...tool.args,
1276
+ type: "code_interpreter"
1277
+ })
1278
+ break
1279
+ }
1280
+ case "openai.file_search": {
1281
+ tools.push({
1282
+ ...tool.args,
1283
+ type: "file_search"
1284
+ })
1285
+ break
1286
+ }
1287
+ case "openai.web_search": {
1288
+ tools.push({
1289
+ ...tool.args,
1290
+ type: "web_search"
1291
+ })
1292
+ break
1293
+ }
1294
+ case "openai.web_search_preview": {
1295
+ tools.push({
1296
+ ...tool.args,
1297
+ type: "web_search_preview"
1298
+ })
1299
+ break
1300
+ }
1301
+ default: {
1302
+ return yield* new AiError.MalformedInput({
1303
+ module: "AnthropicLanguageModel",
1304
+ method: "prepareTools",
1305
+ description: `Received request to call unknown provider-defined tool '${tool.name}'`
1306
+ })
1307
+ }
616
1308
  }
617
1309
  }
618
- })
1310
+ }
1311
+
1312
+ if (options.toolChoice === "auto" || options.toolChoice === "none" || options.toolChoice === "required") {
1313
+ toolChoice = options.toolChoice
1314
+ }
1315
+
1316
+ if (typeof options.toolChoice === "object" && "tool" in options.toolChoice) {
1317
+ toolChoice = Predicate.isUndefined(OpenAiTool.getProviderDefinedToolName(options.toolChoice.tool))
1318
+ ? { type: "function", name: options.toolChoice.tool }
1319
+ : { type: options.toolChoice.tool }
1320
+ }
1321
+
1322
+ return { tools, toolChoice }
1323
+ })
1324
+
1325
+ // =============================================================================
1326
+ // Utilities
1327
+ // =============================================================================
1328
+
1329
+ const isFileId = (data: string, config: Config.Service): boolean =>
1330
+ Predicate.isNotUndefined(config.fileIdPrefixes) && config.fileIdPrefixes.some((prefix) => data.startsWith(prefix))
1331
+
1332
+ const getItemId = (
1333
+ part:
1334
+ | Prompt.TextPart
1335
+ | Prompt.ToolCallPart
1336
+ ): string | undefined => part.options.openai?.itemId
1337
+
1338
+ const getImageDetail = (part: Prompt.FilePart): typeof Generated.InputImageContentDetail.Encoded =>
1339
+ part.options.openai?.imageDetail ?? "auto"
1340
+
1341
+ const prepareInclude = (
1342
+ options: LanguageModel.ProviderOptions,
1343
+ config: Config.Service
1344
+ ): ReadonlyArray<typeof Generated.Includable.Encoded> => {
1345
+ const include: Set<typeof Generated.Includable.Encoded> = new Set(config.include ?? [])
1346
+
1347
+ const codeInterpreterTool = options.tools.find((tool) =>
1348
+ Tool.isProviderDefined(tool) &&
1349
+ tool.id === "openai.code_interpreter"
1350
+ ) as Tool.AnyProviderDefined | undefined
1351
+
1352
+ if (Predicate.isNotUndefined(codeInterpreterTool)) {
1353
+ include.add("code_interpreter_call.outputs")
1354
+ }
1355
+
1356
+ const webSearchTool = options.tools.find((tool) =>
1357
+ Tool.isProviderDefined(tool) &&
1358
+ (tool.id === "openai.web_search" ||
1359
+ tool.id === "openai.web_search_preview")
1360
+ ) as Tool.AnyProviderDefined | undefined
1361
+
1362
+ if (Predicate.isNotUndefined(webSearchTool)) {
1363
+ include.add("web_search_call.action.sources")
1364
+ }
1365
+
1366
+ return Array.from(include)
1367
+ }
1368
+
1369
+ const prepareResponseFormat = (
1370
+ options: LanguageModel.ProviderOptions
1371
+ ): typeof Generated.TextResponseFormatConfiguration.Encoded => {
1372
+ if (options.responseFormat.type === "json") {
1373
+ const name = options.responseFormat.objectName
1374
+ const schema = options.responseFormat.schema
1375
+ return {
1376
+ type: "json_schema",
1377
+ name,
1378
+ description: Tool.getDescriptionFromSchemaAst(schema.ast) ?? "Response with a JSON object",
1379
+ schema: Tool.getJsonSchemaFromSchemaAst(schema.ast) as any,
1380
+ strict: true
1381
+ }
1382
+ }
1383
+ return { type: "text" }
619
1384
  }