@effect/ai-openai 4.0.0-beta.3 → 4.0.0-beta.31

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
  })
@@ -389,6 +390,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
389
390
  )
390
391
 
391
392
  return yield* LanguageModel.make({
393
+ codecTransformer: toCodecOpenAI,
392
394
  generateText: Effect.fnUntraced(
393
395
  function*(options) {
394
396
  const config = yield* makeConfig
@@ -592,7 +594,7 @@ const prepareMessages = Effect.fnUntraced(
592
594
  }
593
595
 
594
596
  if (part.data instanceof Uint8Array) {
595
- const base64 = Base64.encode(part.data)
597
+ const base64 = Encoding.encodeBase64(part.data)
596
598
  const imageUrl = `data:${mediaType};base64,${base64}`
597
599
  content.push({ type: "input_image", image_url: imageUrl, detail })
598
600
  }
@@ -606,7 +608,7 @@ const prepareMessages = Effect.fnUntraced(
606
608
  }
607
609
 
608
610
  if (part.data instanceof Uint8Array) {
609
- const base64 = Base64.encode(part.data)
611
+ const base64 = Encoding.encodeBase64(part.data)
610
612
  const fileName = part.fileName ?? `part-${index}.pdf`
611
613
  const fileData = `data:application/pdf;base64,${base64}`
612
614
  content.push({ type: "input_file", filename: fileName, file_data: fileData })
@@ -1036,10 +1038,11 @@ const makeResponse = Effect.fnUntraced(
1036
1038
 
1037
1039
  case "function_call": {
1038
1040
  hasToolCalls = true
1041
+
1039
1042
  const toolName = part.name
1040
- const toolParams = part.arguments
1041
- const params = yield* Effect.try({
1042
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1043
+
1044
+ const toolParams = yield* Effect.try({
1045
+ try: () => Tool.unsafeSecureJsonParse(part.arguments),
1043
1046
  catch: (cause) =>
1044
1047
  AiError.make({
1045
1048
  module: "OpenAiLanguageModel",
@@ -1051,6 +1054,9 @@ const makeResponse = Effect.fnUntraced(
1051
1054
  })
1052
1055
  })
1053
1056
  })
1057
+
1058
+ const params = yield* transformToolCallParams(options.tools, part.name, toolParams)
1059
+
1054
1060
  parts.push({
1055
1061
  type: "tool-call",
1056
1062
  id: part.call_id,
@@ -1730,11 +1736,14 @@ const makeStreamResponse = Effect.fnUntraced(
1730
1736
 
1731
1737
  case "function_call": {
1732
1738
  delete activeToolCalls[event.output_index]
1739
+
1733
1740
  hasToolCalls = true
1741
+
1734
1742
  const toolName = event.item.name
1735
- const toolParams = event.item.arguments
1736
- const params = yield* Effect.try({
1737
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1743
+ const toolArgs = event.item.arguments
1744
+
1745
+ const toolParams = yield* Effect.try({
1746
+ try: () => Tool.unsafeSecureJsonParse(toolArgs),
1738
1747
  catch: (cause) =>
1739
1748
  AiError.make({
1740
1749
  module: "OpenAiLanguageModel",
@@ -1746,10 +1755,14 @@ const makeStreamResponse = Effect.fnUntraced(
1746
1755
  })
1747
1756
  })
1748
1757
  })
1758
+
1759
+ const params = yield* transformToolCallParams(options.tools, toolName, toolParams)
1760
+
1749
1761
  parts.push({
1750
1762
  type: "tool-params-end",
1751
1763
  id: event.item.call_id
1752
1764
  })
1765
+
1753
1766
  parts.push({
1754
1767
  type: "tool-call",
1755
1768
  id: event.item.call_id,
@@ -1757,6 +1770,7 @@ const makeStreamResponse = Effect.fnUntraced(
1757
1770
  params,
1758
1771
  metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
1759
1772
  })
1773
+
1760
1774
  break
1761
1775
  }
1762
1776
 
@@ -2281,12 +2295,14 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2281
2295
  for (const tool of allowedTools) {
2282
2296
  if (Tool.isUserDefined(tool)) {
2283
2297
  const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true
2298
+ const description = Tool.getDescription(tool)
2299
+ const parameters = yield* tryJsonSchema(tool.parametersSchema, "prepareTools")
2284
2300
  tools.push({
2285
2301
  type: "function",
2286
2302
  name: tool.name,
2287
- description: Tool.getDescription(tool) ?? null,
2288
- parameters: Tool.getJsonSchema(tool) as { readonly [x: string]: Schema.Json },
2289
- strict
2303
+ parameters,
2304
+ strict,
2305
+ ...(Predicate.isNotUndefined(description) ? { description } : undefined)
2290
2306
  })
2291
2307
  }
2292
2308
 
@@ -2480,23 +2496,45 @@ const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefi
2480
2496
  const makeEncryptedContentMetadata = (encryptedContent: string | null | undefined) =>
2481
2497
  Predicate.isNotNullish(encryptedContent) ? { encryptedContent } : undefined
2482
2498
 
2483
- const prepareResponseFormat = ({ config, options }: {
2499
+ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError =>
2500
+ AiError.make({
2501
+ module: "OpenAiLanguageModel",
2502
+ method,
2503
+ reason: new AiError.UnsupportedSchemaError({
2504
+ description: error instanceof Error ? error.message : String(error)
2505
+ })
2506
+ })
2507
+
2508
+ const tryCodecTransform = <S extends Schema.Top>(schema: S, method: string) =>
2509
+ Effect.try({
2510
+ try: () => toCodecOpenAI(schema),
2511
+ catch: (error) => unsupportedSchemaError(error, method)
2512
+ })
2513
+
2514
+ const tryJsonSchema = <S extends Schema.Top>(schema: S, method: string) =>
2515
+ Effect.try({
2516
+ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }),
2517
+ catch: (error) => unsupportedSchemaError(error, method)
2518
+ })
2519
+
2520
+ const prepareResponseFormat = Effect.fnUntraced(function*({ config, options }: {
2484
2521
  readonly config: typeof Config.Service
2485
2522
  readonly options: LanguageModel.ProviderOptions
2486
- }): typeof Generated.TextResponseFormatConfiguration.Encoded => {
2523
+ }): Effect.fn.Return<typeof Generated.TextResponseFormatConfiguration.Encoded, AiError.AiError> {
2487
2524
  if (options.responseFormat.type === "json") {
2488
2525
  const name = options.responseFormat.objectName
2489
2526
  const schema = options.responseFormat.schema
2527
+ const jsonSchema = yield* tryJsonSchema(schema, "prepareResponseFormat")
2490
2528
  return {
2491
2529
  type: "json_schema",
2492
2530
  name,
2493
2531
  description: AST.resolveDescription(schema.ast) ?? "Response with a JSON object",
2494
- schema: Tool.getJsonSchemaFromSchema(schema) as any,
2532
+ schema: jsonSchema,
2495
2533
  strict: config.strictJsonSchema ?? true
2496
2534
  }
2497
2535
  }
2498
2536
  return { type: "text" }
2499
- }
2537
+ })
2500
2538
 
2501
2539
  interface ModelCapabilities {
2502
2540
  readonly isReasoningModel: boolean
@@ -2606,3 +2644,40 @@ const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.U
2606
2644
  }
2607
2645
  }
2608
2646
  }
2647
+
2648
+ const transformToolCallParams = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>(
2649
+ tools: Tools,
2650
+ toolName: string,
2651
+ toolParams: unknown
2652
+ ): Effect.fn.Return<unknown, AiError.AiError> {
2653
+ const tool = tools.find((tool) => tool.name === toolName)
2654
+
2655
+ if (Predicate.isUndefined(tool)) {
2656
+ return yield* AiError.make({
2657
+ module: "OpenAiLanguageModel",
2658
+ method: "makeResponse",
2659
+ reason: new AiError.ToolNotFoundError({
2660
+ toolName,
2661
+ availableTools: tools.map((tool) => tool.name)
2662
+ })
2663
+ })
2664
+ }
2665
+
2666
+ const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse")
2667
+
2668
+ const transform = Schema.decodeEffect(codec)
2669
+
2670
+ return yield* (
2671
+ transform(toolParams) as Effect.Effect<unknown, Schema.SchemaError>
2672
+ ).pipe(Effect.mapError((error) =>
2673
+ AiError.make({
2674
+ module: "OpenAiLanguageModel",
2675
+ method: "makeResponse",
2676
+ reason: new AiError.ToolParameterValidationError({
2677
+ toolName,
2678
+ toolParams,
2679
+ description: error.issue.toString()
2680
+ })
2681
+ })
2682
+ ))
2683
+ })