@effect/ai-openai 4.0.0-beta.2 → 4.0.0-beta.21

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.
@@ -281,7 +281,7 @@ export const layerConfig = (options?: {
281
281
  /**
282
282
  * The config value to load for the API key.
283
283
  */
284
- readonly apiKey?: Config.Config<Redacted.Redacted<string>> | undefined
284
+ readonly apiKey?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
285
285
 
286
286
  /**
287
287
  * The config value to load for the API URL.
@@ -291,12 +291,12 @@ export const layerConfig = (options?: {
291
291
  /**
292
292
  * The config value to load for the organization ID.
293
293
  */
294
- readonly organizationId?: Config.Config<Redacted.Redacted<string>> | undefined
294
+ readonly organizationId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
295
295
 
296
296
  /**
297
297
  * The config value to load for the project ID.
298
298
  */
299
- readonly projectId?: Config.Config<Redacted.Redacted<string>> | undefined
299
+ readonly projectId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
300
300
 
301
301
  /**
302
302
  * Optional transformer for the HTTP client.
@@ -57,51 +57,43 @@ export type OpenAiRateLimitMetadata = OpenAiErrorMetadata & {
57
57
  }
58
58
 
59
59
  declare module "effect/unstable/ai/AiError" {
60
- export interface RateLimitError {
61
- readonly metadata: {
62
- readonly openai?: OpenAiRateLimitMetadata | null
63
- }
60
+ export interface RateLimitErrorMetadata {
61
+ readonly openai?: OpenAiRateLimitMetadata | null
64
62
  }
65
63
 
66
- export interface QuotaExhaustedError {
67
- readonly metadata: {
68
- readonly openai?: OpenAiErrorMetadata | null
69
- }
64
+ export interface QuotaExhaustedErrorMetadata {
65
+ readonly openai?: OpenAiErrorMetadata | null
70
66
  }
71
67
 
72
- export interface AuthenticationError {
73
- readonly metadata: {
74
- readonly openai?: OpenAiErrorMetadata | null
75
- }
68
+ export interface AuthenticationErrorMetadata {
69
+ readonly openai?: OpenAiErrorMetadata | null
76
70
  }
77
71
 
78
- export interface ContentPolicyError {
79
- readonly metadata: {
80
- readonly openai?: OpenAiErrorMetadata | null
81
- }
72
+ export interface ContentPolicyErrorMetadata {
73
+ readonly openai?: OpenAiErrorMetadata | null
82
74
  }
83
75
 
84
- export interface InvalidRequestError {
85
- readonly metadata: {
86
- readonly openai?: OpenAiErrorMetadata | null
87
- }
76
+ export interface InvalidRequestErrorMetadata {
77
+ readonly openai?: OpenAiErrorMetadata | null
88
78
  }
89
79
 
90
- export interface InternalProviderError {
91
- readonly metadata: {
92
- readonly openai?: OpenAiErrorMetadata | null
93
- }
80
+ export interface InternalProviderErrorMetadata {
81
+ readonly openai?: OpenAiErrorMetadata | null
94
82
  }
95
83
 
96
- export interface InvalidOutputError {
97
- readonly metadata: {
98
- readonly openai?: OpenAiErrorMetadata | null
99
- }
84
+ export interface InvalidOutputErrorMetadata {
85
+ readonly openai?: OpenAiErrorMetadata | null
100
86
  }
101
87
 
102
- export interface UnknownError {
103
- readonly metadata: {
104
- readonly openai?: OpenAiErrorMetadata | null
105
- }
88
+ export interface StructuredOutputErrorMetadata {
89
+ readonly openai?: OpenAiErrorMetadata | null
90
+ }
91
+
92
+ export interface UnsupportedSchemaErrorMetadata {
93
+ readonly openai?: OpenAiErrorMetadata | null
94
+ }
95
+
96
+ export interface UnknownErrorMetadata {
97
+ readonly openai?: OpenAiErrorMetadata | null
106
98
  }
107
99
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import * as DateTime from "effect/DateTime"
10
10
  import * as Effect from "effect/Effect"
11
- import * as Base64 from "effect/encoding/Base64"
11
+ import * as Encoding from "effect/Encoding"
12
12
  import { dual } from "effect/Function"
13
13
  import * as Layer from "effect/Layer"
14
14
  import * as Predicate from "effect/Predicate"
@@ -23,6 +23,7 @@ import * as AiError from "effect/unstable/ai/AiError"
23
23
  import * as IdGenerator from "effect/unstable/ai/IdGenerator"
24
24
  import * as LanguageModel from "effect/unstable/ai/LanguageModel"
25
25
  import * as AiModel from "effect/unstable/ai/Model"
26
+ import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput"
26
27
  import type * as Prompt from "effect/unstable/ai/Prompt"
27
28
  import type * as Response from "effect/unstable/ai/Response"
28
29
  import * as Tool from "effect/unstable/ai/Tool"
@@ -319,7 +320,7 @@ export const model = (
319
320
  model: (string & {}) | Model,
320
321
  config?: Omit<typeof Config.Service, "model">
321
322
  ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> =>
322
- AiModel.make("openai", layer({ model, config }))
323
+ AiModel.make("openai", model, layer({ model, config }))
323
324
 
324
325
  // TODO
325
326
  // /**
@@ -330,7 +331,7 @@ export const model = (
330
331
  // model: (string & {}) | Model,
331
332
  // config?: Omit<typeof Config.Service, "model">
332
333
  // ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
333
- // AiModel.make("openai", layerWithTokenizer({ model, config }))
334
+ // AiModel.make("openai", model, layerWithTokenizer({ model, config }))
334
335
 
335
336
  /**
336
337
  * Creates an OpenAI language model service.
@@ -369,7 +370,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
369
370
  options,
370
371
  toolNameMapper
371
372
  })
372
- const responseFormat = prepareResponseFormat({
373
+ const responseFormat = yield* prepareResponseFormat({
373
374
  config,
374
375
  options
375
376
  })
@@ -429,7 +430,10 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
429
430
  })
430
431
  )
431
432
  )
432
- })
433
+ }).pipe(Effect.provideService(
434
+ LanguageModel.CurrentCodecTransformer,
435
+ toCodecOpenAI
436
+ ))
433
437
  })
434
438
 
435
439
  /**
@@ -592,7 +596,7 @@ const prepareMessages = Effect.fnUntraced(
592
596
  }
593
597
 
594
598
  if (part.data instanceof Uint8Array) {
595
- const base64 = Base64.encode(part.data)
599
+ const base64 = Encoding.encodeBase64(part.data)
596
600
  const imageUrl = `data:${mediaType};base64,${base64}`
597
601
  content.push({ type: "input_image", image_url: imageUrl, detail })
598
602
  }
@@ -606,7 +610,7 @@ const prepareMessages = Effect.fnUntraced(
606
610
  }
607
611
 
608
612
  if (part.data instanceof Uint8Array) {
609
- const base64 = Base64.encode(part.data)
613
+ const base64 = Encoding.encodeBase64(part.data)
610
614
  const fileName = part.fileName ?? `part-${index}.pdf`
611
615
  const fileData = `data:application/pdf;base64,${base64}`
612
616
  content.push({ type: "input_file", filename: fileName, file_data: fileData })
@@ -1036,10 +1040,11 @@ const makeResponse = Effect.fnUntraced(
1036
1040
 
1037
1041
  case "function_call": {
1038
1042
  hasToolCalls = true
1043
+
1039
1044
  const toolName = part.name
1040
- const toolParams = part.arguments
1041
- const params = yield* Effect.try({
1042
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1045
+
1046
+ const toolParams = yield* Effect.try({
1047
+ try: () => Tool.unsafeSecureJsonParse(part.arguments),
1043
1048
  catch: (cause) =>
1044
1049
  AiError.make({
1045
1050
  module: "OpenAiLanguageModel",
@@ -1051,6 +1056,9 @@ const makeResponse = Effect.fnUntraced(
1051
1056
  })
1052
1057
  })
1053
1058
  })
1059
+
1060
+ const params = yield* transformToolCallParams(options.tools, part.name, toolParams)
1061
+
1054
1062
  parts.push({
1055
1063
  type: "tool-call",
1056
1064
  id: part.call_id,
@@ -1730,11 +1738,14 @@ const makeStreamResponse = Effect.fnUntraced(
1730
1738
 
1731
1739
  case "function_call": {
1732
1740
  delete activeToolCalls[event.output_index]
1741
+
1733
1742
  hasToolCalls = true
1743
+
1734
1744
  const toolName = event.item.name
1735
- const toolParams = event.item.arguments
1736
- const params = yield* Effect.try({
1737
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1745
+ const toolArgs = event.item.arguments
1746
+
1747
+ const toolParams = yield* Effect.try({
1748
+ try: () => Tool.unsafeSecureJsonParse(toolArgs),
1738
1749
  catch: (cause) =>
1739
1750
  AiError.make({
1740
1751
  module: "OpenAiLanguageModel",
@@ -1746,10 +1757,14 @@ const makeStreamResponse = Effect.fnUntraced(
1746
1757
  })
1747
1758
  })
1748
1759
  })
1760
+
1761
+ const params = yield* transformToolCallParams(options.tools, toolName, toolParams)
1762
+
1749
1763
  parts.push({
1750
1764
  type: "tool-params-end",
1751
1765
  id: event.item.call_id
1752
1766
  })
1767
+
1753
1768
  parts.push({
1754
1769
  type: "tool-call",
1755
1770
  id: event.item.call_id,
@@ -1757,6 +1772,7 @@ const makeStreamResponse = Effect.fnUntraced(
1757
1772
  params,
1758
1773
  metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
1759
1774
  })
1775
+
1760
1776
  break
1761
1777
  }
1762
1778
 
@@ -2281,12 +2297,14 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2281
2297
  for (const tool of allowedTools) {
2282
2298
  if (Tool.isUserDefined(tool)) {
2283
2299
  const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true
2300
+ const description = Tool.getDescription(tool)
2301
+ const parameters = yield* tryJsonSchema(tool.parametersSchema, "prepareTools")
2284
2302
  tools.push({
2285
2303
  type: "function",
2286
2304
  name: tool.name,
2287
- description: Tool.getDescription(tool) ?? null,
2288
- parameters: Tool.getJsonSchema(tool) as { readonly [x: string]: Schema.Json },
2289
- strict
2305
+ parameters,
2306
+ strict,
2307
+ ...(Predicate.isNotUndefined(description) ? { description } : undefined)
2290
2308
  })
2291
2309
  }
2292
2310
 
@@ -2480,23 +2498,45 @@ const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefi
2480
2498
  const makeEncryptedContentMetadata = (encryptedContent: string | null | undefined) =>
2481
2499
  Predicate.isNotNullish(encryptedContent) ? { encryptedContent } : undefined
2482
2500
 
2483
- const prepareResponseFormat = ({ config, options }: {
2501
+ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError =>
2502
+ AiError.make({
2503
+ module: "OpenAiLanguageModel",
2504
+ method,
2505
+ reason: new AiError.UnsupportedSchemaError({
2506
+ description: error instanceof Error ? error.message : String(error)
2507
+ })
2508
+ })
2509
+
2510
+ const tryCodecTransform = <S extends Schema.Top>(schema: S, method: string) =>
2511
+ Effect.try({
2512
+ try: () => toCodecOpenAI(schema),
2513
+ catch: (error) => unsupportedSchemaError(error, method)
2514
+ })
2515
+
2516
+ const tryJsonSchema = <S extends Schema.Top>(schema: S, method: string) =>
2517
+ Effect.try({
2518
+ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }),
2519
+ catch: (error) => unsupportedSchemaError(error, method)
2520
+ })
2521
+
2522
+ const prepareResponseFormat = Effect.fnUntraced(function*({ config, options }: {
2484
2523
  readonly config: typeof Config.Service
2485
2524
  readonly options: LanguageModel.ProviderOptions
2486
- }): typeof Generated.TextResponseFormatConfiguration.Encoded => {
2525
+ }): Effect.fn.Return<typeof Generated.TextResponseFormatConfiguration.Encoded, AiError.AiError> {
2487
2526
  if (options.responseFormat.type === "json") {
2488
2527
  const name = options.responseFormat.objectName
2489
2528
  const schema = options.responseFormat.schema
2529
+ const jsonSchema = yield* tryJsonSchema(schema, "prepareResponseFormat")
2490
2530
  return {
2491
2531
  type: "json_schema",
2492
2532
  name,
2493
2533
  description: AST.resolveDescription(schema.ast) ?? "Response with a JSON object",
2494
- schema: Tool.getJsonSchemaFromSchema(schema) as any,
2534
+ schema: jsonSchema,
2495
2535
  strict: config.strictJsonSchema ?? true
2496
2536
  }
2497
2537
  }
2498
2538
  return { type: "text" }
2499
- }
2539
+ })
2500
2540
 
2501
2541
  interface ModelCapabilities {
2502
2542
  readonly isReasoningModel: boolean
@@ -2606,3 +2646,40 @@ const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.U
2606
2646
  }
2607
2647
  }
2608
2648
  }
2649
+
2650
+ const transformToolCallParams = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>(
2651
+ tools: Tools,
2652
+ toolName: string,
2653
+ toolParams: unknown
2654
+ ): Effect.fn.Return<unknown, AiError.AiError> {
2655
+ const tool = tools.find((tool) => tool.name === toolName)
2656
+
2657
+ if (Predicate.isUndefined(tool)) {
2658
+ return yield* AiError.make({
2659
+ module: "OpenAiLanguageModel",
2660
+ method: "makeResponse",
2661
+ reason: new AiError.ToolNotFoundError({
2662
+ toolName,
2663
+ availableTools: tools.map((tool) => tool.name)
2664
+ })
2665
+ })
2666
+ }
2667
+
2668
+ const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse")
2669
+
2670
+ const transform = Schema.decodeEffect(codec)
2671
+
2672
+ return yield* (
2673
+ transform(toolParams) as Effect.Effect<unknown, Schema.SchemaError>
2674
+ ).pipe(Effect.mapError((error) =>
2675
+ AiError.make({
2676
+ module: "OpenAiLanguageModel",
2677
+ method: "makeResponse",
2678
+ reason: new AiError.ToolParameterValidationError({
2679
+ toolName,
2680
+ toolParams,
2681
+ description: error.issue.toString()
2682
+ })
2683
+ })
2684
+ ))
2685
+ })