@effect/ai-openai 4.0.0-beta.4 → 4.0.0-beta.41

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.
@@ -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,9 +8,10 @@
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
+ import * as Option from "effect/Option"
14
15
  import * as Predicate from "effect/Predicate"
15
16
  import * as Redactable from "effect/Redactable"
16
17
  import * as Schema from "effect/Schema"
@@ -23,6 +24,7 @@ import * as AiError from "effect/unstable/ai/AiError"
23
24
  import * as IdGenerator from "effect/unstable/ai/IdGenerator"
24
25
  import * as LanguageModel from "effect/unstable/ai/LanguageModel"
25
26
  import * as AiModel from "effect/unstable/ai/Model"
27
+ import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput"
26
28
  import type * as Prompt from "effect/unstable/ai/Prompt"
27
29
  import type * as Response from "effect/unstable/ai/Response"
28
30
  import * as Tool from "effect/unstable/ai/Tool"
@@ -319,7 +321,7 @@ export const model = (
319
321
  model: (string & {}) | Model,
320
322
  config?: Omit<typeof Config.Service, "model">
321
323
  ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> =>
322
- AiModel.make("openai", layer({ model, config }))
324
+ AiModel.make("openai", model, layer({ model, config }))
323
325
 
324
326
  // TODO
325
327
  // /**
@@ -330,7 +332,7 @@ export const model = (
330
332
  // model: (string & {}) | Model,
331
333
  // config?: Omit<typeof Config.Service, "model">
332
334
  // ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
333
- // AiModel.make("openai", layerWithTokenizer({ model, config }))
335
+ // AiModel.make("openai", model, layerWithTokenizer({ model, config }))
334
336
 
335
337
  /**
336
338
  * Creates an OpenAI language model service.
@@ -369,7 +371,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
369
371
  options,
370
372
  toolNameMapper
371
373
  })
372
- const responseFormat = prepareResponseFormat({
374
+ const responseFormat = yield* prepareResponseFormat({
373
375
  config,
374
376
  options
375
377
  })
@@ -381,14 +383,16 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
381
383
  verbosity: config.text?.verbosity ?? null,
382
384
  format: responseFormat
383
385
  },
384
- ...(Predicate.isNotUndefined(tools) ? { tools } : undefined),
385
- ...(Predicate.isNotUndefined(toolChoice) ? { tool_choice: toolChoice } : undefined)
386
+ ...(tools ? { tools } : undefined),
387
+ ...(toolChoice ? { tool_choice: toolChoice } : undefined),
388
+ ...(options.previousResponseId ? { previous_response_id: options.previousResponseId } : undefined)
386
389
  }
387
390
  return request
388
391
  }
389
392
  )
390
393
 
391
394
  return yield* LanguageModel.make({
395
+ codecTransformer: toCodecOpenAI,
392
396
  generateText: Effect.fnUntraced(
393
397
  function*(options) {
394
398
  const config = yield* makeConfig
@@ -547,16 +551,17 @@ const prepareMessages = Effect.fnUntraced(
547
551
  if (config.store === false && capabilities.isReasoningModel) {
548
552
  include.add("reasoning.encrypted_content")
549
553
  }
550
- if (Predicate.isNotUndefined(codeInterpreterTool)) {
554
+ if (codeInterpreterTool) {
551
555
  include.add("code_interpreter_call.outputs")
552
556
  }
553
- if (Predicate.isNotUndefined(webSearchTool) || Predicate.isNotUndefined(webSearchPreviewTool)) {
557
+ if (webSearchTool || webSearchPreviewTool) {
554
558
  include.add("web_search_call.action.sources")
555
559
  }
556
560
 
557
561
  const messages: Array<typeof Generated.InputItem.Encoded> = []
562
+ const prompt = options.incrementalPrompt ?? options.prompt
558
563
 
559
- for (const message of options.prompt.content) {
564
+ for (const message of prompt.content) {
560
565
  switch (message.role) {
561
566
  case "system": {
562
567
  messages.push({
@@ -592,7 +597,7 @@ const prepareMessages = Effect.fnUntraced(
592
597
  }
593
598
 
594
599
  if (part.data instanceof Uint8Array) {
595
- const base64 = Base64.encode(part.data)
600
+ const base64 = Encoding.encodeBase64(part.data)
596
601
  const imageUrl = `data:${mediaType};base64,${base64}`
597
602
  content.push({ type: "input_image", image_url: imageUrl, detail })
598
603
  }
@@ -606,7 +611,7 @@ const prepareMessages = Effect.fnUntraced(
606
611
  }
607
612
 
608
613
  if (part.data instanceof Uint8Array) {
609
- const base64 = Base64.encode(part.data)
614
+ const base64 = Encoding.encodeBase64(part.data)
610
615
  const fileName = part.fileName ?? `part-${index}.pdf`
611
616
  const fileData = `data:application/pdf;base64,${base64}`
612
617
  content.push({ type: "input_file", filename: fileName, file_data: fileData })
@@ -921,7 +926,7 @@ const buildHttpRequestDetails = (
921
926
  method: request.method,
922
927
  url: request.url,
923
928
  urlParams: Array.from(request.urlParams),
924
- hash: request.hash,
929
+ hash: Option.getOrUndefined(request.hash),
925
930
  headers: Redactable.redact(request.headers) as Record<string, string>
926
931
  })
927
932
 
@@ -1036,10 +1041,11 @@ const makeResponse = Effect.fnUntraced(
1036
1041
 
1037
1042
  case "function_call": {
1038
1043
  hasToolCalls = true
1044
+
1039
1045
  const toolName = part.name
1040
- const toolParams = part.arguments
1041
- const params = yield* Effect.try({
1042
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1046
+
1047
+ const toolParams = yield* Effect.try({
1048
+ try: () => Tool.unsafeSecureJsonParse(part.arguments),
1043
1049
  catch: (cause) =>
1044
1050
  AiError.make({
1045
1051
  module: "OpenAiLanguageModel",
@@ -1051,6 +1057,9 @@ const makeResponse = Effect.fnUntraced(
1051
1057
  })
1052
1058
  })
1053
1059
  })
1060
+
1061
+ const params = yield* transformToolCallParams(options.tools, part.name, toolParams)
1062
+
1054
1063
  parts.push({
1055
1064
  type: "tool-call",
1056
1065
  id: part.call_id,
@@ -1370,16 +1379,42 @@ const makeStreamResponse = Effect.fnUntraced(
1370
1379
  // Track annotations for current message to include in text-end metadata
1371
1380
  const activeAnnotations: Array<typeof Generated.Annotation.Encoded> = []
1372
1381
 
1382
+ type ReasoningSummaryPartStatus = "active" | "can-conclude" | "concluded"
1383
+ type ReasoningPart = {
1384
+ encryptedContent: string | undefined
1385
+ summaryParts: Record<number, ReasoningSummaryPartStatus>
1386
+ }
1387
+
1373
1388
  // Track active reasoning items with state machine for proper concluding logic
1374
- const activeReasoning: Record<string, {
1375
- readonly encryptedContent: string | undefined
1376
- readonly summaryParts: Record<number, "active" | "can-conclude" | "concluded">
1377
- }> = {}
1389
+ const activeReasoning: Record<string, ReasoningPart> = {}
1390
+
1391
+ const getOrCreateReasoningPart = (
1392
+ itemId: string,
1393
+ encryptedContent?: string | null
1394
+ ): ReasoningPart => {
1395
+ const activePart = activeReasoning[itemId]
1396
+ if (Predicate.isNotUndefined(activePart)) {
1397
+ if (Predicate.isNotNullish(encryptedContent)) {
1398
+ activePart.encryptedContent = encryptedContent
1399
+ }
1400
+ return activePart
1401
+ }
1402
+
1403
+ const reasoningPart: ReasoningPart = {
1404
+ encryptedContent: Predicate.isNotNullish(encryptedContent) ? encryptedContent : undefined,
1405
+ summaryParts: {}
1406
+ }
1407
+ activeReasoning[itemId] = reasoningPart
1408
+ return reasoningPart
1409
+ }
1378
1410
 
1379
1411
  // Track active tool calls with optional provider-specific state
1380
1412
  const activeToolCalls: Record<number, {
1381
1413
  readonly id: string
1382
1414
  readonly name: string
1415
+ readonly functionCall?: {
1416
+ emitted: boolean
1417
+ }
1383
1418
  readonly applyPatch?: {
1384
1419
  hasDiff: boolean
1385
1420
  endEmitted: boolean
@@ -1529,7 +1564,8 @@ const makeStreamResponse = Effect.fnUntraced(
1529
1564
  case "function_call": {
1530
1565
  activeToolCalls[event.output_index] = {
1531
1566
  id: event.item.call_id,
1532
- name: event.item.name
1567
+ name: event.item.name,
1568
+ functionCall: { emitted: false }
1533
1569
  }
1534
1570
  parts.push({
1535
1571
  type: "tool-params-start",
@@ -1572,21 +1608,20 @@ const makeStreamResponse = Effect.fnUntraced(
1572
1608
  }
1573
1609
 
1574
1610
  case "reasoning": {
1575
- const encryptedContent = event.item.encrypted_content ?? undefined
1576
- activeReasoning[event.item.id] = {
1577
- encryptedContent,
1578
- summaryParts: { 0: "active" }
1579
- }
1580
- parts.push({
1581
- type: "reasoning-start",
1582
- id: `${event.item.id}:0`,
1583
- metadata: {
1584
- openai: {
1585
- ...makeItemIdMetadata(event.item.id),
1586
- ...makeEncryptedContentMetadata(event.item.encrypted_content)
1611
+ const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content)
1612
+ if (Predicate.isUndefined(reasoningPart.summaryParts[0])) {
1613
+ reasoningPart.summaryParts[0] = "active"
1614
+ parts.push({
1615
+ type: "reasoning-start",
1616
+ id: `${event.item.id}:0`,
1617
+ metadata: {
1618
+ openai: {
1619
+ ...makeItemIdMetadata(event.item.id),
1620
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
1621
+ }
1587
1622
  }
1588
- }
1589
- })
1623
+ })
1624
+ }
1590
1625
  break
1591
1626
  }
1592
1627
 
@@ -1729,12 +1764,20 @@ const makeStreamResponse = Effect.fnUntraced(
1729
1764
  }
1730
1765
 
1731
1766
  case "function_call": {
1767
+ const toolCall = activeToolCalls[event.output_index]
1768
+ if (Predicate.isNotUndefined(toolCall?.functionCall?.emitted) && toolCall.functionCall.emitted) {
1769
+ delete activeToolCalls[event.output_index]
1770
+ break
1771
+ }
1732
1772
  delete activeToolCalls[event.output_index]
1773
+
1733
1774
  hasToolCalls = true
1775
+
1734
1776
  const toolName = event.item.name
1735
- const toolParams = event.item.arguments
1736
- const params = yield* Effect.try({
1737
- try: () => Tool.unsafeSecureJsonParse(toolParams),
1777
+ const toolArgs = event.item.arguments
1778
+
1779
+ const toolParams = yield* Effect.try({
1780
+ try: () => Tool.unsafeSecureJsonParse(toolArgs),
1738
1781
  catch: (cause) =>
1739
1782
  AiError.make({
1740
1783
  module: "OpenAiLanguageModel",
@@ -1746,10 +1789,14 @@ const makeStreamResponse = Effect.fnUntraced(
1746
1789
  })
1747
1790
  })
1748
1791
  })
1792
+
1793
+ const params = yield* transformToolCallParams(options.tools, toolName, toolParams)
1794
+
1749
1795
  parts.push({
1750
1796
  type: "tool-params-end",
1751
1797
  id: event.item.call_id
1752
1798
  })
1799
+
1753
1800
  parts.push({
1754
1801
  type: "tool-call",
1755
1802
  id: event.item.call_id,
@@ -1757,6 +1804,7 @@ const makeStreamResponse = Effect.fnUntraced(
1757
1804
  params,
1758
1805
  metadata: { openai: { ...makeItemIdMetadata(event.item.id) } }
1759
1806
  })
1807
+
1760
1808
  break
1761
1809
  }
1762
1810
 
@@ -1862,7 +1910,7 @@ const makeStreamResponse = Effect.fnUntraced(
1862
1910
  }
1863
1911
 
1864
1912
  case "reasoning": {
1865
- const reasoningPart = activeReasoning[event.item.id]
1913
+ const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content)
1866
1914
  for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
1867
1915
  if (status === "active" || status === "can-conclude") {
1868
1916
  parts.push({
@@ -1871,7 +1919,7 @@ const makeStreamResponse = Effect.fnUntraced(
1871
1919
  metadata: {
1872
1920
  openai: {
1873
1921
  ...makeItemIdMetadata(event.item.id),
1874
- ...makeEncryptedContentMetadata(event.item.encrypted_content)
1922
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
1875
1923
  }
1876
1924
  }
1877
1925
  })
@@ -2006,6 +2054,48 @@ const makeStreamResponse = Effect.fnUntraced(
2006
2054
  break
2007
2055
  }
2008
2056
 
2057
+ case "response.function_call_arguments.done": {
2058
+ const toolCall = activeToolCalls[event.output_index]
2059
+ if (
2060
+ Predicate.isNotUndefined(toolCall?.functionCall) &&
2061
+ !toolCall.functionCall.emitted
2062
+ ) {
2063
+ hasToolCalls = true
2064
+
2065
+ const toolParams = yield* Effect.try({
2066
+ try: () => Tool.unsafeSecureJsonParse(event.arguments),
2067
+ catch: (cause) =>
2068
+ AiError.make({
2069
+ module: "OpenAiLanguageModel",
2070
+ method: "makeStreamResponse",
2071
+ reason: new AiError.ToolParameterValidationError({
2072
+ toolName: toolCall.name,
2073
+ toolParams: {},
2074
+ description: `Failed securely JSON parse tool parameters: ${cause}`
2075
+ })
2076
+ })
2077
+ })
2078
+
2079
+ const params = yield* transformToolCallParams(options.tools, toolCall.name, toolParams)
2080
+
2081
+ parts.push({
2082
+ type: "tool-params-end",
2083
+ id: toolCall.id
2084
+ })
2085
+
2086
+ parts.push({
2087
+ type: "tool-call",
2088
+ id: toolCall.id,
2089
+ name: toolCall.name,
2090
+ params,
2091
+ metadata: { openai: { ...makeItemIdMetadata(event.item_id) } }
2092
+ })
2093
+
2094
+ toolCall.functionCall.emitted = true
2095
+ }
2096
+ break
2097
+ }
2098
+
2009
2099
  case "response.apply_patch_call_operation_diff.delta": {
2010
2100
  const toolCall = activeToolCalls[event.output_index]
2011
2101
  if (Predicate.isNotUndefined(toolCall?.applyPatch)) {
@@ -2095,28 +2185,28 @@ const makeStreamResponse = Effect.fnUntraced(
2095
2185
  }
2096
2186
 
2097
2187
  case "response.reasoning_summary_part.added": {
2098
- // The first reasoning start is pushed in the `response.output_item.added` block
2188
+ const reasoningPart = getOrCreateReasoningPart(event.item_id)
2099
2189
  if (event.summary_index > 0) {
2100
- const reasoningPart = activeReasoning[event.item_id]
2101
- if (Predicate.isNotUndefined(reasoningPart)) {
2102
- // Conclude all can-conclude parts before starting new one
2103
- for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
2104
- if (status === "can-conclude") {
2105
- parts.push({
2106
- type: "reasoning-end",
2107
- id: `${event.item_id}:${summaryIndex}`,
2108
- metadata: {
2109
- openai: {
2110
- ...makeItemIdMetadata(event.item_id),
2111
- ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
2112
- }
2190
+ // Conclude all can-conclude parts before starting new one
2191
+ for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) {
2192
+ if (status === "can-conclude") {
2193
+ parts.push({
2194
+ type: "reasoning-end",
2195
+ id: `${event.item_id}:${summaryIndex}`,
2196
+ metadata: {
2197
+ openai: {
2198
+ ...makeItemIdMetadata(event.item_id),
2199
+ ...makeEncryptedContentMetadata(reasoningPart.encryptedContent)
2113
2200
  }
2114
- })
2115
- reasoningPart.summaryParts[Number(summaryIndex)] = "concluded"
2116
- }
2201
+ }
2202
+ })
2203
+ reasoningPart.summaryParts[Number(summaryIndex)] = "concluded"
2117
2204
  }
2118
- reasoningPart.summaryParts[event.summary_index] = "active"
2119
2205
  }
2206
+ }
2207
+
2208
+ if (Predicate.isUndefined(reasoningPart.summaryParts[event.summary_index])) {
2209
+ reasoningPart.summaryParts[event.summary_index] = "active"
2120
2210
  parts.push({
2121
2211
  type: "reasoning-start",
2122
2212
  id: `${event.item_id}:${event.summary_index}`,
@@ -2142,6 +2232,7 @@ const makeStreamResponse = Effect.fnUntraced(
2142
2232
  }
2143
2233
 
2144
2234
  case "response.reasoning_summary_part.done": {
2235
+ const reasoningPart = getOrCreateReasoningPart(event.item_id)
2145
2236
  // When OpenAI stores message data, we can immediately conclude the
2146
2237
  // reasoning part given that we do not need the encrypted content
2147
2238
  if (config.store === true) {
@@ -2151,11 +2242,11 @@ const makeStreamResponse = Effect.fnUntraced(
2151
2242
  metadata: { openai: { ...makeItemIdMetadata(event.item_id) } }
2152
2243
  })
2153
2244
  // Mark the summary part concluded
2154
- activeReasoning[event.item_id].summaryParts[event.summary_index] = "concluded"
2245
+ reasoningPart.summaryParts[event.summary_index] = "concluded"
2155
2246
  } else {
2156
2247
  // Mark the summary part as can-conclude given we still need a
2157
2248
  // final summary part with the encrypted content
2158
- activeReasoning[event.item_id].summaryParts[event.summary_index] = "can-conclude"
2249
+ reasoningPart.summaryParts[event.summary_index] = "can-conclude"
2159
2250
  }
2160
2251
  break
2161
2252
  }
@@ -2281,12 +2372,14 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
2281
2372
  for (const tool of allowedTools) {
2282
2373
  if (Tool.isUserDefined(tool)) {
2283
2374
  const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true
2375
+ const description = Tool.getDescription(tool)
2376
+ const parameters = yield* tryJsonSchema(tool.parametersSchema, "prepareTools")
2284
2377
  tools.push({
2285
2378
  type: "function",
2286
2379
  name: tool.name,
2287
- description: Tool.getDescription(tool) ?? null,
2288
- parameters: Tool.getJsonSchema(tool) as { readonly [x: string]: Schema.Json },
2289
- strict
2380
+ parameters,
2381
+ strict,
2382
+ ...(Predicate.isNotUndefined(description) ? { description } : undefined)
2290
2383
  })
2291
2384
  }
2292
2385
 
@@ -2480,23 +2573,45 @@ const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefi
2480
2573
  const makeEncryptedContentMetadata = (encryptedContent: string | null | undefined) =>
2481
2574
  Predicate.isNotNullish(encryptedContent) ? { encryptedContent } : undefined
2482
2575
 
2483
- const prepareResponseFormat = ({ config, options }: {
2576
+ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError =>
2577
+ AiError.make({
2578
+ module: "OpenAiLanguageModel",
2579
+ method,
2580
+ reason: new AiError.UnsupportedSchemaError({
2581
+ description: error instanceof Error ? error.message : String(error)
2582
+ })
2583
+ })
2584
+
2585
+ const tryCodecTransform = <S extends Schema.Top>(schema: S, method: string) =>
2586
+ Effect.try({
2587
+ try: () => toCodecOpenAI(schema),
2588
+ catch: (error) => unsupportedSchemaError(error, method)
2589
+ })
2590
+
2591
+ const tryJsonSchema = <S extends Schema.Top>(schema: S, method: string) =>
2592
+ Effect.try({
2593
+ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }),
2594
+ catch: (error) => unsupportedSchemaError(error, method)
2595
+ })
2596
+
2597
+ const prepareResponseFormat = Effect.fnUntraced(function*({ config, options }: {
2484
2598
  readonly config: typeof Config.Service
2485
2599
  readonly options: LanguageModel.ProviderOptions
2486
- }): typeof Generated.TextResponseFormatConfiguration.Encoded => {
2600
+ }): Effect.fn.Return<typeof Generated.TextResponseFormatConfiguration.Encoded, AiError.AiError> {
2487
2601
  if (options.responseFormat.type === "json") {
2488
2602
  const name = options.responseFormat.objectName
2489
2603
  const schema = options.responseFormat.schema
2604
+ const jsonSchema = yield* tryJsonSchema(schema, "prepareResponseFormat")
2490
2605
  return {
2491
2606
  type: "json_schema",
2492
2607
  name,
2493
2608
  description: AST.resolveDescription(schema.ast) ?? "Response with a JSON object",
2494
- schema: Tool.getJsonSchemaFromSchema(schema) as any,
2609
+ schema: jsonSchema,
2495
2610
  strict: config.strictJsonSchema ?? true
2496
2611
  }
2497
2612
  }
2498
2613
  return { type: "text" }
2499
- }
2614
+ })
2500
2615
 
2501
2616
  interface ModelCapabilities {
2502
2617
  readonly isReasoningModel: boolean
@@ -2606,3 +2721,40 @@ const getUsage = (usage: Generated.ResponseUsage | null | undefined): Response.U
2606
2721
  }
2607
2722
  }
2608
2723
  }
2724
+
2725
+ const transformToolCallParams = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>(
2726
+ tools: Tools,
2727
+ toolName: string,
2728
+ toolParams: unknown
2729
+ ): Effect.fn.Return<unknown, AiError.AiError> {
2730
+ const tool = tools.find((tool) => tool.name === toolName)
2731
+
2732
+ if (Predicate.isUndefined(tool)) {
2733
+ return yield* AiError.make({
2734
+ module: "OpenAiLanguageModel",
2735
+ method: "makeResponse",
2736
+ reason: new AiError.ToolNotFoundError({
2737
+ toolName,
2738
+ availableTools: tools.map((tool) => tool.name)
2739
+ })
2740
+ })
2741
+ }
2742
+
2743
+ const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse")
2744
+
2745
+ const transform = Schema.decodeEffect(codec)
2746
+
2747
+ return yield* (
2748
+ transform(toolParams) as Effect.Effect<unknown, Schema.SchemaError>
2749
+ ).pipe(Effect.mapError((error) =>
2750
+ AiError.make({
2751
+ module: "OpenAiLanguageModel",
2752
+ method: "makeResponse",
2753
+ reason: new AiError.ToolParameterValidationError({
2754
+ toolName,
2755
+ toolParams,
2756
+ description: error.issue.toString()
2757
+ })
2758
+ })
2759
+ ))
2760
+ })
package/src/index.ts CHANGED
@@ -24,6 +24,15 @@ export * as OpenAiClient from "./OpenAiClient.ts"
24
24
  */
25
25
  export * as OpenAiConfig from "./OpenAiConfig.ts"
26
26
 
27
+ /**
28
+ * OpenAI Embedding Model implementation.
29
+ *
30
+ * Provides an EmbeddingModel implementation for OpenAI's embeddings API.
31
+ *
32
+ * @since 1.0.0
33
+ */
34
+ export * as OpenAiEmbeddingModel from "./OpenAiEmbeddingModel.ts"
35
+
27
36
  /**
28
37
  * OpenAI error metadata augmentation.
29
38
  *
@@ -161,12 +161,14 @@ export const parseRateLimitHeaders = (headers: Record<string, string>) => {
161
161
  let retryAfter: Duration.Duration | undefined
162
162
  if (Predicate.isNotUndefined(retryAfterRaw)) {
163
163
  const parsed = Number.parse(retryAfterRaw)
164
- if (Predicate.isNotUndefined(parsed)) {
165
- retryAfter = Duration.seconds(parsed)
164
+ if (Option.isSome(parsed)) {
165
+ retryAfter = Duration.seconds(parsed.value)
166
166
  }
167
167
  }
168
168
  const remainingRaw = headers["x-ratelimit-remaining-requests"]
169
- const remaining = Predicate.isNotUndefined(remainingRaw) ? Number.parse(remainingRaw) ?? null : null
169
+ const remaining = Predicate.isNotUndefined(remainingRaw)
170
+ ? Option.getOrNull(Number.parse(remainingRaw))
171
+ : null
170
172
  return {
171
173
  retryAfter,
172
174
  limit: headers["x-ratelimit-limit-requests"] ?? null,
@@ -187,7 +189,7 @@ export const buildHttpRequestDetails = (
187
189
  method: request.method,
188
190
  url: request.url,
189
191
  urlParams: Array.from(request.urlParams),
190
- hash: request.hash,
192
+ hash: Option.getOrUndefined(request.hash),
191
193
  headers: Redactable.redact(request.headers) as Record<string, string>
192
194
  })
193
195